Skip to content

Commit dc3e404

Browse files
committed
Merge branch 'misc'
2 parents e189a5c + 80cba10 commit dc3e404

File tree

2 files changed

+300
-2
lines changed

2 files changed

+300
-2
lines changed

docs/11-proposals/PROPOSAL_marketplace_modules.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,12 @@ func importMarketplaceModule(t *testing.T, componentID string, mprPath string) {
201201

202202
### Phase 2: Authentication
203203

204+
See dedicated proposal: [`PROPOSAL_platform_auth.md`](PROPOSAL_platform_auth.md). The auth layer is shared with Deploy API and future platform-API consumers.
205+
204206
- `mxcli auth login/logout/status` commands
205-
- Credential storage at `~/.mxcli/auth.json`
207+
- Credential storage at `~/.mxcli/auth.json` (plus OS keychain)
206208
- Environment variable support for CI
207-
- Authenticated HTTP client factory
209+
- Authenticated HTTP client factory with per-host scheme routing (PAT for Content API / marketplace; API key for Deploy API)
208210

209211
### Phase 3: Install & Download
210212

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Proposal: `mxcli auth` — Mendix Platform Authentication
2+
3+
**Status:** Draft
4+
**Date:** 2026-04-14
5+
**Author:** Generated with Claude Code
6+
7+
## Problem
8+
9+
A growing set of `mxcli` features need to talk to Mendix platform APIs on behalf of the user:
10+
11+
- **Marketplace** — downloading `.mpk` files (see `PROPOSAL_marketplace_modules.md`)
12+
- **Deploy API** — building, packaging, and deploying apps to Mendix Cloud
13+
- **Team Server, Content API, Platform APIs** — future features (project metadata, users, environments)
14+
15+
Each of these APIs uses a different authentication header scheme, and today `mxcli` has no concept of stored credentials. Hard-coding auth into each feature would produce duplicated credential-storage code, inconsistent UX, and no single place for the user to run `mxcli auth login`.
16+
17+
This proposal specifies a shared `internal/auth` package and `mxcli auth` command set that every platform-API consumer can use.
18+
19+
## Mendix Authentication Schemes
20+
21+
Based on current Mendix documentation (2026-04):
22+
23+
| Scheme | Header(s) | APIs | Obtained From |
24+
|---|---|---|---|
25+
| **PAT** (Personal Access Token) | `Authorization: MxToken <pat>` | Content API (marketplace/modules), Platform APIs | Mendix portal → Developer Settings → Personal Access Tokens |
26+
| **API Key** | `Mendix-UserName: <email>`<br>`Mendix-ApiKey: <key>` | Deploy API, Team Server | Mendix portal → [Profile → API Keys](https://docs.mendix.com/portal/user-settings/#profile-api-keys) |
27+
28+
References:
29+
- Content API / marketplace: <https://docs.mendix.com/apidocs-mxsdk/apidocs/content-api/> (PAT)
30+
- Deploy API: requires API key per <https://docs.mendix.com/portal/user-settings/#profile-api-keys>
31+
32+
API keys "allow apps using them to act on behalf of the user who created the key" with the same privileges. Revocation may take time to propagate due to caching.
33+
34+
### Azure AD / Entra ID — future consideration
35+
36+
Mendix's portal login uses Azure AD SSO. This does **not** mean third-party CLIs can authenticate against AAD directly — doing so requires a public AAD app registration that Mendix has authorized for CLI use, and no such app ID is publicly documented today. For v1, the realistic flow is:
37+
38+
1. User signs into the portal (AAD SSO in the browser, Mendix's problem)
39+
2. User creates a PAT or API key in the portal
40+
3. User pastes the token into `mxcli auth login`
41+
42+
An AAD device-code flow (`mxcli login` opens a browser, no token pasting) remains a Phase 5 stretch goal contingent on Mendix publishing a CLI client ID. The package is designed so this can drop in later without breaking callers.
43+
44+
## Design
45+
46+
### Package Layout
47+
48+
```
49+
internal/auth/
50+
├── credential.go # Credential struct, Scheme enum
51+
├── scheme.go # Host → scheme mapping
52+
├── resolver.go # Env vars → keychain → file priority
53+
├── client.go # *http.Client factory that injects the right headers
54+
├── store.go # Store interface
55+
├── store_file.go # ~/.mxcli/auth.json (chmod 0600), always available
56+
├── store_keyring.go # OS keychain via zalando/go-keyring (pure Go, no CGO)
57+
└── errors.go # ErrUnauthenticated, ErrNoCredential, ErrSchemeMismatch
58+
59+
cmd/mxcli/
60+
└── auth.go # login / logout / status / list subcommands
61+
```
62+
63+
### Credential Model
64+
65+
```go
66+
package auth
67+
68+
type Scheme string
69+
70+
const (
71+
SchemePAT Scheme = "pat" // MxToken header
72+
SchemeAPIKey Scheme = "apikey" // Mendix-UserName + Mendix-ApiKey
73+
)
74+
75+
type Credential struct {
76+
Profile string `json:"profile"` // "default", "deploy-ci", ...
77+
Scheme Scheme `json:"scheme"`
78+
Username string `json:"username,omitempty"` // required for apikey
79+
Token string `json:"token"` // never logged
80+
CreatedAt time.Time `json:"created_at"`
81+
Label string `json:"label,omitempty"` // free-form note
82+
}
83+
```
84+
85+
**Named profiles from day one.** Users with multiple Mendix tenants, or who need separate personal vs. CI credentials, need profile support, and retrofitting it is painful. Default profile name is `default`.
86+
87+
A single profile holds **one** credential. Users who need both a PAT (marketplace) and an API key (deploy) under the same identity create two profiles (`--profile deploy`) — or we store them as a pair under one profile if field testing shows that's common. Starting with one-credential-per-profile keeps the model simple.
88+
89+
### Scheme Routing (Host → Scheme)
90+
91+
Callers shouldn't know which header to set. The client picks based on target hostname:
92+
93+
```go
94+
// internal/auth/scheme.go
95+
var hostSchemes = map[string]Scheme{
96+
"appstore.home.mendix.com": SchemePAT,
97+
"cloud.home.mendix.com": SchemePAT,
98+
"deploy.mendix.com": SchemeAPIKey,
99+
// ...add as new APIs are integrated
100+
}
101+
102+
func schemeForHost(host string) (Scheme, bool) {
103+
s, ok := hostSchemes[host]
104+
return s, ok
105+
}
106+
```
107+
108+
If a request targets a host whose scheme doesn't match the resolved credential's scheme, the client returns `ErrSchemeMismatch` with a hint: "host deploy.mendix.com needs an API key; profile 'default' has a PAT. Run `mxcli auth login --profile deploy --api-key`."
109+
110+
### Authenticated HTTP Client
111+
112+
```go
113+
// internal/auth/client.go
114+
func ClientFor(ctx context.Context, profile string) (*http.Client, error) {
115+
cred, err := Resolve(ctx, profile)
116+
if err != nil {
117+
return nil, err
118+
}
119+
return &http.Client{
120+
Transport: &authTransport{cred: cred, inner: http.DefaultTransport},
121+
Timeout: 30 * time.Second,
122+
}, nil
123+
}
124+
125+
type authTransport struct {
126+
cred *Credential
127+
inner http.RoundTripper
128+
}
129+
130+
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
131+
scheme, known := schemeForHost(req.URL.Host)
132+
if !known {
133+
return nil, fmt.Errorf("unknown Mendix host: %s", req.URL.Host)
134+
}
135+
if scheme != t.cred.Scheme {
136+
return nil, &ErrSchemeMismatch{Host: req.URL.Host, Need: scheme, Have: t.cred.Scheme}
137+
}
138+
req = req.Clone(req.Context())
139+
switch scheme {
140+
case SchemePAT:
141+
req.Header.Set("Authorization", "MxToken "+t.cred.Token)
142+
case SchemeAPIKey:
143+
req.Header.Set("Mendix-UserName", t.cred.Username)
144+
req.Header.Set("Mendix-ApiKey", t.cred.Token)
145+
}
146+
resp, err := t.inner.RoundTrip(req)
147+
if resp != nil && resp.StatusCode == 401 {
148+
// Wrap as typed error so callers can show a helpful hint
149+
return resp, &ErrUnauthenticated{Profile: t.cred.Profile}
150+
}
151+
return resp, err
152+
}
153+
```
154+
155+
### Storage Priority
156+
157+
The resolver walks these in order, first match wins:
158+
159+
1. **Environment variables** (highest priority, for CI):
160+
- `MENDIX_PAT` → PAT credential
161+
- `MENDIX_USERNAME` + `MENDIX_API_KEY` → API key credential
162+
- `MXCLI_PROFILE` selects which profile env vars populate (default: `default`)
163+
2. **OS keychain** via `github.com/zalando/go-keyring` (pure Go, no CGO — matches CLAUDE.md). Service name `mxcli`, account = profile name. Works on macOS Keychain, Linux Secret Service, Windows Credential Manager.
164+
3. **File fallback** at `~/.mxcli/auth.json`, `chmod 0600`. Necessary for environments without a keyring backend (devcontainers, many CI runners).
165+
166+
Users can force the file backend with `mxcli auth login --no-keychain` — useful for shared devcontainers.
167+
168+
File format (supports multiple profiles):
169+
170+
```json
171+
{
172+
"version": 1,
173+
"profiles": {
174+
"default": {
175+
"scheme": "pat",
176+
"token": "xxxxxxxx…",
177+
"created_at": "2026-04-14T12:00:00Z"
178+
},
179+
"deploy-ci": {
180+
"scheme": "apikey",
181+
"username": "ci@company.com",
182+
"token": "xxxxxxxx…",
183+
"created_at": "2026-04-14T12:05:00Z"
184+
}
185+
}
186+
}
187+
```
188+
189+
### CLI Commands
190+
191+
```
192+
mxcli auth login [--pat | --api-key] [--profile NAME] [--no-keychain]
193+
[--token TOKEN] [--username EMAIL] # non-interactive
194+
mxcli auth logout [--profile NAME] [--all]
195+
mxcli auth status [--profile NAME] [--json]
196+
mxcli auth list # all profiles
197+
```
198+
199+
#### `mxcli auth login` (interactive)
200+
201+
```
202+
$ mxcli auth login
203+
Mendix authentication scheme:
204+
[1] Personal Access Token (PAT) — recommended for marketplace, content API
205+
[2] API Key — required for Deploy API, Team Server
206+
Choice [1]: 2
207+
208+
Email: user@company.com
209+
API Key: ****************************
210+
Validating... ✓ authenticated as user@company.com
211+
Store in: [1] OS keychain [2] ~/.mxcli/auth.json
212+
Choice [1]: 1
213+
Saved credential to profile 'default' (scheme: apikey)
214+
```
215+
216+
- Token is read with `golang.org/x/term` (no echo).
217+
- Validation hits a cheap, idempotent authenticated endpoint on the scheme's primary host, e.g. `GET https://deploy.mendix.com/api/1/apps` (just to verify the credential works, discarding the response). The exact validation endpoint per scheme is picked during implementation — anything that returns 200 on success and 401 on bad creds.
218+
- Validation failures do not store the credential.
219+
220+
#### `mxcli auth login` (non-interactive, for CI)
221+
222+
```bash
223+
mxcli auth login --pat --token "$MENDIX_PAT" --profile default
224+
mxcli auth login --api-key --username ci@company.com --token "$MENDIX_API_KEY" --profile deploy
225+
```
226+
227+
Or skip `login` entirely and rely on `MENDIX_PAT` / `MENDIX_USERNAME`+`MENDIX_API_KEY` env vars — the resolver picks them up without any stored credential.
228+
229+
#### `mxcli auth status`
230+
231+
```
232+
$ mxcli auth status
233+
Profile: default
234+
Scheme: pat
235+
Source: keychain
236+
Created: 2026-04-14 12:00:00 UTC
237+
Identity: user@company.com (verified 2s ago)
238+
```
239+
240+
With `--json` for scripting. Performs a live credential check unless `--offline` is passed.
241+
242+
### Consumer Example (Marketplace)
243+
244+
```go
245+
// cmd/mxcli/cmd_marketplace.go
246+
func installModule(ctx context.Context, componentID string) error {
247+
client, err := auth.ClientFor(ctx, auth.ProfileFromEnv())
248+
if err != nil {
249+
return fmt.Errorf("not authenticated: %w\nrun: mxcli auth login", err)
250+
}
251+
252+
url := fmt.Sprintf("https://appstore.home.mendix.com/rest/packagesapi/v2/packages/%s", componentID)
253+
resp, err := client.Get(url)
254+
// ...
255+
}
256+
```
257+
258+
No consumer code knows about PATs, API keys, env vars, keychains, or header names.
259+
260+
## Phased Rollout
261+
262+
| Phase | Deliverable | Unblocks |
263+
|---|---|---|
264+
| **1** | `internal/auth` package: Credential, file store, env-var resolver, Authenticator with scheme routing. Keychain store is optional (file fallback covers all environments). | Everything downstream |
265+
| **1.5** | Discovery spike: validate that `appstore.home.mendix.com` accepts `MxToken` PATs as documented (and that `deploy.mendix.com` accepts `Mendix-ApiKey`). One day, parallel to Phase 1. | De-risks Phase 3/4 |
266+
| **2** | `mxcli auth login/logout/status/list` commands. Interactive scheme selection, validation ping before store. | Users can authenticate |
267+
| **3** | Keychain store (`zalando/go-keyring`), prompt-based migration from file to keychain. | Better security on laptops |
268+
| **4** | Wire into `mxcli marketplace` commands (see `PROPOSAL_marketplace_modules.md` Phase 3). | Marketplace install works |
269+
| **5** | Wire into Deploy API (`mxcli deploy` — separate future proposal). | App deployment works |
270+
| **6** (stretch) | AAD device-code flow via MSAL Go, contingent on Mendix publishing a public CLI client ID. Drop-in replacement for the PAT-paste flow in `login`. | `mxcli auth login` without leaving the terminal |
271+
272+
Phases 1 and 2 are independent of the marketplace/deploy features and can ship first. The `internal/auth` package design does not change if Phase 6 later adds AAD — AAD tokens are still credentials, stored with the same profile mechanism; only the acquisition path is new.
273+
274+
## Security Considerations
275+
276+
- **File storage**: `chmod 0600` on create; refuse to read if permissions are more permissive (warn the user to `chmod 0600` the file).
277+
- **Token redaction**: Tokens must never appear in logs, `status` output, `--verbose` traces, or error messages. `Credential.String()` returns `"<scheme> token=REDACTED"`.
278+
- **Process env leakage**: When a credential comes from `MENDIX_PAT`, do not shell out with the env var inherited unless the child process is a trusted Mendix tool (`mx`). Wrap `exec.Command` callers to scrub these vars by default.
279+
- **Revocation caching**: Mendix notes that API key revocation may take time to propagate. `auth status` should surface this ("credential may be cached upstream; revoke in portal for immediate effect") so users aren't surprised.
280+
- **No telemetry**: The auth package does not emit session logs containing credentials or user identity. Only anonymized metrics (success/failure counts) if anything.
281+
282+
## Open Questions
283+
284+
1. **One credential per profile, or both PAT + API key per profile?** One-per-profile is simpler; real usage will tell us if users routinely juggle both under one identity. Revisit after Phase 4.
285+
2. **Project-local profile override?** A `./.mxcli/config.yaml` with `auth_profile: deploy-ci` lets teams pin project-to-profile without env vars. Defer until we see the need.
286+
3. **Credential rotation UX.** `mxcli auth rotate` that walks the user through revoking an old PAT and generating a new one via the portal. Nice-to-have, not v1.
287+
4. **AAD client ID.** Ask Mendix DevRel whether a public CLI app registration exists or is planned. If yes, Phase 6 becomes a real v1 feature; if no, we ship without it and users generate PATs in the portal (the status quo).
288+
5. **Encrypted file backend?** If keychain isn't available, should the file backend encrypt at rest (e.g., age/libsodium with a passphrase)? Adds complexity; most CLIs (`gh`, `doctl`, `az`) rely on filesystem perms alone. Defer.
289+
290+
## References
291+
292+
- Content API authentication: <https://docs.mendix.com/apidocs-mxsdk/apidocs/content-api/>
293+
- Profile API keys: <https://docs.mendix.com/portal/user-settings/#profile-api-keys>
294+
- Marketplace modules proposal: `PROPOSAL_marketplace_modules.md`
295+
- RFC 8628 (device authorization grant, relevant for Phase 6): <https://datatracker.ietf.org/doc/html/rfc8628>
296+
- `zalando/go-keyring`: <https://github.com/zalando/go-keyring>

0 commit comments

Comments
 (0)