Repository navigation
[CP-768] feat(auth): add login with endpoint-inferred Auth0 issuer - #219
Open
qdrant-cloud-bot wants to merge 28 commits into
Open
qdrant-cloud-bot wants to merge 28 commits into
qdrant-cloud-bot wants to merge 28 commits into
Conversation
Discover the login URL from QDRANT_CLOUD_ENDPOINT (grpc.<cluster> → login.<cluster>), preferring unauthenticated API gateway protected-resource metadata when OAuth is enabled.
Robert-Stam
reviewed
Oct 10, 2026
Present browser login and API keys as alternatives, drop Auth0 and non-prod hosts from generated help.
1 of 2 tasks
* [CP-768] fix(auth): send audience on device-code login Auth0 does not apply the resource parameter on /oauth/device/code, so that request must use audience. The poll stays a plain device-code grant, backs off on slow_down, and the loopback callback checks iss when Auth0 sends it. * test(auth): keep require out of http handlers in device login test golangci-lint (testifylint go-require, wsl_v5) flagged the device-login test. A form parse error now returns 400 from the handler, which still fails the login. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Robert-Stam <robert.stam@qdrant.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Auth0 matches a loopback callback's port exactly, so a random port fails with Callback URL mismatch. Try 48271, 48272 and 48273 in order; each is registered on the CLI's Auth0 application. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each Auth0 tenant generates its own client id for the native app, and a native app's id is public, so ship them keyed by issuer: users no longer pass --client-id. Development is set; staging and production are TBD until their apps exist, and ask for --client-id meanwhile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A self-contained page (inline Qdrant mark and terminal icon, no fonts, scripts or other requests, light and dark) for success and failure, instead of plain text. The failure detail is HTML-escaped; strict CSP. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ogin Same grid background, wordmark above a white card, title and tiles as the Auth0 Universal Login pages. Still self-contained: the grid image and logos are inline, the system font replaces the web font. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… was not used On development, discover fell back to endpoint inference while curl got the metadata; the reason was swallowed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ail_verified iam-service's ExchangeToken requires email_verified in the Auth0 access token. The custom-token-claims Action sets it, but Auth0 silently drops it: email_verified is a reserved OpenID Connect claim name. With the email scope Auth0 adds it itself, as for the Cloud console. Found on development: invalid_grant: email not verified in JWT payload. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a login, qcloud lists the accounts once. When the login was granted exactly one, it is stored with the session and used when no flag, environment variable or context sets an account; auth status shows it. With an API key nothing is looked up, and a failed lookup never fails the login. Needs cluster-api's ListAccounts to return only the grant's accounts to OAuth callers (qdrant-cloud-cluster-api#5223). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The login printed its discovery details (login URL, source, resource, scope), which auth discover and auth status already show, and a waiting line. Now: the browser line with its fallback URL, then one result line, e.g. "Logged in as ada@example.com, account Production." The email is read from the token for display only. The device flow prints only the URL and the code. Warnings stay on stderr. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The token's scope is what qcloud requested (manage by default); the consent page can narrow it. The post-login Action now puts the granted scope in the token (https://cloud.qdrant.io/granted_scope). qcloud reads it at login, stores it as granted_scope next to requested_scope in credentials.yaml (the stored scope field is renamed accordingly), and shows it: "Logged in as ..., account ..., read only." and in auth status. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…omes from auth status now names the active context, says when an API key is set (commands then use it instead of the login), and shows the account with its source: --account-id, QDRANT_CLOUD_ACCOUNT_ID, the context, or the login. OAuth sessions stay keyed by endpoint; a context selects them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The requested scope (what qcloud asked Auth0 for) next to the granted one only confused; it stays in credentials.yaml and auth token --json. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
auth login creates an OAuth context (auth: oauth, no API key) for every account granted on the consent page, named <env>-<account name>. With one account it becomes the current context unless one is already set; with several, the login lists them for qcloud context use. A later login updates them and removes those of accounts no longer granted; auth logout removes them. API key contexts and other endpoints' contexts are never touched; a name clash gets a suffix. A context now has exactly one credential: an API key (or command) or --oauth, never both, never none. context show/list show the credential. The account stored on the OAuth session is gone: contexts replace it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e from login OAuth contexts are created and updated by qcloud auth login only; a wrong one is fixed by logging in again. context set now refuses to change a context the login manages, and only creates API key contexts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They follow the login: auth login creates and updates them, auth logout removes them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… current As with one account, unless a context is already current. The login marks the current context with * in its list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ope error When the gateway refuses a call only because the OAuth access is read only (ErrorInfo INSUFFICIENT_SCOPE, qdrant-cloud-api-gateway#694), qcloud adds: Run "qcloud auth login --scope manage" and allow manage on the consent page. Role denials get no hint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The access and refresh tokens go to the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service on Linux) via go-keyring, as two entries per endpoint. credentials.yaml keeps only what is not secret, with store: keychain. - Without a keychain (headless Linux, containers, CI) the tokens fall back to credentials.yaml (0600) and login warns. - QDRANT_CLOUD_TOKEN_STORE=keychain requires the keychain; =file skips it. - A session stored in the file moves to the keychain on its next refresh. - Logout removes the keychain entries; auth status does not read them. - Test binaries use an in-memory store, never the machine's keychain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Robert-Stam
reviewed
Oct 23, 2026
Robert-Stam
reviewed
Oct 23, 2026
Robert-Stam
reviewed
Oct 23, 2026
Comment on lines
+10
to
+11
| "https://login.cloud.qdrant.io/": clientIDTBD, | ||
| "https://login.staging-cloud.qdrant.io/": clientIDTBD, |
Robert-Stam
reviewed
Oct 23, 2026
Comment on lines
+17
to
+18
| "https://login.cloud.qdrant.io/", // TBD | ||
| "https://login.staging-cloud.qdrant.io/", // TBD |
Robert-Stam
reviewed
Oct 23, 2026
Robert-Stam
reviewed
Oct 23, 2026
…mes in comments Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ation_pending
The token error kept only the description, and the poll looked for the code in
it. Auth0's description ("User has yet to authorize device code.") does not
contain the code, so the first poll ended the login. TokenError keeps both.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot open When the browser opens, login prints one line. When it cannot be opened, it prints the URL and a --device hint and keeps waiting, instead of failing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
asadrizv
reviewed
Oct 23, 2026
Comment on lines
+67
to
+74
| if len(prm.AuthorizationServers) > 0 && prm.AuthorizationServers[0] != "" { | ||
| out.Issuer = canonicalizeIssuer(prm.AuthorizationServers[0]) | ||
| out.Source = sourceProtectedResource | ||
| } | ||
|
|
||
| if prm.Resource != "" { | ||
| out.Resource = strings.TrimRight(prm.Resource, "/") | ||
| } |
There was a problem hiding this comment.
one for before merge: discover trusts prm.resource + authorization_servers as-is, so a non-qdrant --endpoint could get a real token sent back to it. wdyt about ignoring the doc unless resource == hosts.Resource() (rfc 9728 §3.3)? rest looks good 🙌
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
qcloud auth discover|login|logout|status|token.QDRANT_CLOUD_ENDPOINT/--endpoint(grpc.<cluster>.qdrant.io→login.<cluster>.qdrant.io).https://api.<cluster>.qdrant.io/.well-known/oauth-protected-resourcewhen OAuth is enabled; fall back to hostname inference when it is not.--device. The loopback listener uses the first free port of 48271, 48272 and 48273: Auth0 matches a loopback callback's port exactly (a random port gets "Callback URL mismatch"), so the threehttp://127.0.0.1:<port>/callbackURLs are registered on the CLI's Auth0 application. tokens incredentials.yaml(0600). API keys still win when set. gRPC sendsAuthorization: Bearerafter login.auth: oauth, no API key), named<env>-<account name>(no prefix for production). One account: it becomes the current context unless one is already set. Several: the login lists them forqcloud context use. A later login updates them and removes those of accounts no longer granted;auth logoutremoves them. API key contexts are never touched. A context has exactly one credential: an API key (or command), or OAuth for the contexts the login creates.context setonly creates API key contexts and refuses to change OAuth ones (log in again instead);context show/listshow the credential.auth statusshows the active context and where the account comes from.scopeis the requested scope (manageby default); the consent page can narrow it. The post-login Action puts the granted scope in the token ashttps://cloud.qdrant.io/granted_scope(qdrant-cloud-iam-service#543, qdrant-cloud-ui#3238); qcloud storesrequested_scopeandgranted_scopeincredentials.yaml, prints e.g.Logged in as ada@example.com, account Production, read only (context production).and shows both inauth status. Without the claim (before those are deployed) the scope is simply not printed.auth discover/auth status.grpc.development-cloud.qdrant.io:443https://login.development-cloud.qdrant.io/grpc.staging-cloud.qdrant.io:443https://login.staging-cloud.qdrant.io/grpc.cloud.qdrant.io:443https://login.cloud.qdrant.io/The "Qdrant Cloud CLI" client id is built in per login URL (
internal/oauth/clientid.go): each Auth0 tenant generates its own, and a native app's id is public. Development is set; staging and production areTBDuntil their apps exist, and ask for--client-id/QDRANT_CLOUD_OAUTH_CLIENT_IDmeanwhile.qcloud auth discoverneeds no client id.Test plan
qcloud auth discover --endpoint grpc.development-cloud.qdrant.io:443(inference when PRM is down)golangci-lint+go teston affected packagesqcloud auth login --endpoint grpc.development-cloud.qdrant.io:443(no--client-id)protected-resource metadata