Summary
acmctl oauth (browser-based PKCE flow against altinity.auth0.com) is fully implemented client-side and reaches a valid Auth0 id_token, but ACM's POST /api/singleauth rejects that token with HTTP 403 Bad credentials.
Because of this, acmctl oauth cannot complete the login until the ACM backend is taught to accept Native-client tokens.
Reproducer
-
A Native Auth0 application is configured on altinity.auth0.com:
-
acmctl oauth does the standard authorization-code-with-PKCE flow:
- Generates
code_verifier, S256 code_challenge, random state
- Listens on
http://localhost:49152/cb
- Opens browser to
https://altinity.auth0.com/authorize?...&client_id=BjVCq...
- User signs in via Google SSO
- Auth0 redirects to the localhost callback with
?code=...&state=...
- acmctl exchanges code at Auth0
/oauth/token (with code_verifier) and gets a valid id_token
-
acmctl POSTs the id_token to ACM:
POST /api/singleauth
Content-Type: application/x-www-form-urlencoded
token=<id_token>
-
ACM responds:
HTTP/2 403
{"error":"Bad credentials"}
We also tried {token, code, state} (full PKCE shape) — same Bad credentials.
What's happening
/singleauth is built for ACM's own server-side OAuth flow:
- Web UI calls
GET /api/singleauth → ACM generates state + code_verifier server-side, returns the Auth0 authorize URL
- Browser does the Auth0 dance, lands on
https://acm.altinity.cloud/singleauth?code=...&state=...
- Web UI POSTs that code+state to
/api/singleauth
- ACM looks up the state in its server-side store, exchanges code with Auth0 using the stored verifier, mints a session token
For a CLI-initiated PKCE flow, none of that state exists in ACM. The id_token we hold is signed by the same Auth0 tenant but was minted by a different client (the Native CLI app), so ACM's signature/audience check rejects it.
Proposed fix
Pick one — both unblock acmctl with similar effort:
A) Accept Native-client id_tokens on /singleauth (smallest change)
Extend the existing endpoint to accept a {token: <id_token>}-only request shape:
- JWT-validate against
altinity.auth0.com JWKS
- Verify
aud matches an allow-list of trusted CLI client IDs (start with BjVCqpdbU4Z6zYpxHPXG1zU4N6QCIlGX)
- Verify
exp > now, iss == https://altinity.auth0.com/
- Look up the user by Auth0
sub claim (or email if sub isn't mapped)
- Issue an ACM session token, return as today
B) New endpoint /api/cli-auth (cleaner isolation)
Same shape as A, but a separate code path so /singleauth's existing behavior is untouched. Slightly more surface area for slightly less coupling.
Why this is OK security-wise
Trusting id_tokens from a specific Native client_id is the standard pattern. The id_token's signature proves it was issued by altinity.auth0.com; the aud claim proves it was minted for our specific Native app; the audit trail of who can authenticate to that app lives in Auth0's own connection allow-list. All major auth providers (Auth0, Okta, Azure AD) document this pattern for CLI tools.
Acmctl-side status
The CLI code is committed and works up to the /singleauth call:
pkg/api/oauth.go — the OAuth flow (PKCE + loopback listener + Auth0 token exchange)
cmd/oauth.go — the user-facing acmctl oauth command
- Profile machinery (so the resulting token saves to the right
prod/dev/stage profile) is independently working
When this issue is fixed and ACM accepts the id_token, no acmctl change should be needed beyond what's already on master (commits 29baec2, 35424e3).
Workaround until fixed
Use ACM_API_KEY from 1Password (resolve at shell start), or acmctl login --token <key> to write a manually-minted API key into the profile. acmctl oauth errors with the explicit "Bad credentials" message and a hint pointing at this issue.
Summary
acmctl oauth(browser-based PKCE flow againstaltinity.auth0.com) is fully implemented client-side and reaches a valid Auth0id_token, but ACM'sPOST /api/singleauthrejects that token with HTTP 403Bad credentials.Because of this,
acmctl oauthcannot complete the login until the ACM backend is taught to accept Native-client tokens.Reproducer
A Native Auth0 application is configured on
altinity.auth0.com:acmctl oauthdoes the standard authorization-code-with-PKCE flow:code_verifier, S256code_challenge, random statehttp://localhost:49152/cbhttps://altinity.auth0.com/authorize?...&client_id=BjVCq...?code=...&state=.../oauth/token(withcode_verifier) and gets a validid_tokenacmctl POSTs the id_token to ACM:
ACM responds:
We also tried
{token, code, state}(full PKCE shape) — sameBad credentials.What's happening
/singleauthis built for ACM's own server-side OAuth flow:GET /api/singleauth→ ACM generates state + code_verifier server-side, returns the Auth0 authorize URLhttps://acm.altinity.cloud/singleauth?code=...&state=.../api/singleauthFor a CLI-initiated PKCE flow, none of that state exists in ACM. The id_token we hold is signed by the same Auth0 tenant but was minted by a different client (the Native CLI app), so ACM's signature/audience check rejects it.
Proposed fix
Pick one — both unblock acmctl with similar effort:
A) Accept Native-client id_tokens on
/singleauth(smallest change)Extend the existing endpoint to accept a
{token: <id_token>}-only request shape:altinity.auth0.comJWKSaudmatches an allow-list of trusted CLI client IDs (start withBjVCqpdbU4Z6zYpxHPXG1zU4N6QCIlGX)exp > now,iss == https://altinity.auth0.com/subclaim (oremailifsubisn't mapped)B) New endpoint
/api/cli-auth(cleaner isolation)Same shape as A, but a separate code path so
/singleauth's existing behavior is untouched. Slightly more surface area for slightly less coupling.Why this is OK security-wise
Trusting id_tokens from a specific Native client_id is the standard pattern. The id_token's signature proves it was issued by
altinity.auth0.com; theaudclaim proves it was minted for our specific Native app; the audit trail of who can authenticate to that app lives in Auth0's own connection allow-list. All major auth providers (Auth0, Okta, Azure AD) document this pattern for CLI tools.Acmctl-side status
The CLI code is committed and works up to the
/singleauthcall:pkg/api/oauth.go— the OAuth flow (PKCE + loopback listener + Auth0 token exchange)cmd/oauth.go— the user-facingacmctl oauthcommandprod/dev/stage profile) is independently working
When this issue is fixed and ACM accepts the id_token, no acmctl change should be needed beyond what's already on
master(commits29baec2,35424e3).Workaround until fixed
Use
ACM_API_KEYfrom 1Password (resolve at shell start), oracmctl login --token <key>to write a manually-minted API key into the profile.acmctl oautherrors with the explicit "Bad credentials" message and a hint pointing at this issue.