Skip to content

OAuth: /singleauth rejects id_tokens minted by Native CLI client #1

@BorisTyshkevich

Description

@BorisTyshkevich

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

  1. A Native Auth0 application is configured on altinity.auth0.com:

  2. 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
  3. acmctl POSTs the id_token to ACM:

    POST /api/singleauth
    Content-Type: application/x-www-form-urlencoded
    token=<id_token>
    
  4. 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:

  1. Web UI calls GET /api/singleauth → ACM generates state + code_verifier server-side, returns the Auth0 authorize URL
  2. Browser does the Auth0 dance, lands on https://acm.altinity.cloud/singleauth?code=...&state=...
  3. Web UI POSTs that code+state to /api/singleauth
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions