feat: organization selection for browser OAuth logins via x-org-id (ADR-0009) - #183
Conversation
Browser OAuth logins now act in a chosen Logto organization instead of silently falling to the account's platform-side default: - login requests urn:logto:scope:organizations and discovers memberships from userinfo (one resource-less refresh mints the opaque token userinfo accepts; the rotated refresh token is persisted under the refresh lock, the API access token untouched) - one membership selects itself; several prompt on a terminal or take --org (id or name); discovery failing never fails a login — the platform default applies until a selection is made - the selection is stored on the OAuth session and rides x-org-id on every API request, where the platform verifies membership per request - new: ade auth org list / ade auth org switch (no browser round-trip); login --org on an already-authenticated target switches in place - status shows the selection; API keys send no header (already organization-bound) ADR-0009 records the header-vs-organization-token trade-off and the flip condition for moving to signed organization_id claims later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Answers the api-key question in the docs themselves: a key acts in the organization it was created in — no selection stored, none accepted — and OAuth sessions bill to the selected organization. SKILL.md warns agents that re-running an invocation after an org switch joins the recorded run instead of re-billing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds OAuth organization discovery, selection, switching, and x-org-id request routing.
Changes:
- Adds organization-aware OAuth login and management commands.
- Sends stored organization IDs through the gateway.
- Updates tests, help, documentation, and ADR-0009.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/ade_cli/auth.py |
Implements organization selection and commands. |
src/ade_cli/oauth.py |
Discovers memberships and persists selections. |
src/ade_cli/credentials.py |
Exposes the active organization ID. |
src/ade_cli/gateway.py |
Adds the organization request header. |
src/ade_cli/parse.py |
Passes organization context to requests. |
src/ade_cli/extract.py |
Passes organization context to requests. |
src/ade_cli/help.py |
Documents commands and result shapes. |
tests/test_auth_oauth.py |
Tests organization-aware authentication. |
tests/test_help.py |
Updates help command expectations. |
tests/test_telemetry.py |
Registers new command paths. |
README.md |
Documents organization workflows. |
SKILL.md |
Adds agent-facing organization guidance. |
CONTEXT.md |
Defines organization-selection concepts. |
docs/adr/0009-org-selection-rides-x-org-id.md |
Records the transport decision. |
docs/reference/help.json |
Regenerates command reference data. |
login-stderr.log |
Empty file with no functional change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ale selections Addresses review feedback on #183: - _post_token normalizes a 200 whose body is not a JSON object into _TokenEndpointError, so a malformed answer can no longer escape as a decode error. This is what the ADR-0009 guarantee needed (discovery failure never costs a login) and it hardens the login exchange and refresh at the same time — both already translate that error. - userinfo decoding and the organizations claim are validated the same way, raising OrgDiscoveryError instead of AttributeError/JSONDecodeError. - the already-authenticated login path publishes organization (null included), matching the shape help.json advertises for auth login. - org list detects a selection that is no longer a membership: the header keeps going out and the platform rejects it, so the listing says so instead of implying the platform default. - new ade auth org clear makes that remediation real — deliberately offline, since a stale selection is exactly when listing may fail. Also removes login-stderr.log, committed by accident, and ignores *.log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review follow-ups pushed in d80aec4 — all four Copilot comments were valid and are fixed, each with tests verified load-bearing (reverting the fix fails them). Response-shape hardening. Contract parity. The already-authenticated Stale selections. Also removed Suite is 740 passing (18 new tests); ruff and ty clean; |
Bumps `pyproject.toml` and `uv.lock` to 1.0.5 (minimal two-line diff; the lockfile edit is validated by `uv sync --locked`, full suite green, `ade version` reports 1.0.5). ##⚠️ Merge order **Merge #183 first, then this PR** — v1.0.5 is intended to include: - #183 — feat: organization selection for browser OAuth logins via `x-org-id` (ADR-0009) Once #183 is in, cut the release via **Actions → Release → "Run workflow"** on `main`. The pipeline runs the live integration suite (macOS + Windows, against production) before tagging `v1.0.5`. ## Release note candidate Browser (OAuth) sign-in now acts in a chosen organization: a single membership is selected automatically, several prompt at login (or take `--org`), and `ade auth org list` / `ade auth org switch` manage the selection afterwards without a re-login. Requests — and credits — go to the selected organization. API-key logins are unaffected; a key already acts in the organization it was created in. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Browser OAuth logins were organization-blind: the CLI requested no organization at the token grant, so multi-org accounts always landed on the platform-side default (
migratedOrgId) with no way to choose. This adds organization selection and switching, carried as thex-org-idheader the platform authz already honors and membership-verifies for user-scoped tokens.urn:logto:scope:organizationsand discovers memberships via Logto userinfo. One subtlety: the API access token is audience-bound (RFC 8707) and userinfo rejects it, so discovery spends one resource-less refresh to mint the opaque token userinfo accepts — persisting the rotated refresh token under the existing cross-process lock, leaving the API token untouched.--org <id-or-name>. Discovery failure never fails a login — the platform default applies with a remediation note in both the human text and the JSON payload.ade auth org list(live memberships, selected one marked) andade auth org switch <org>— no browser round-trip, since the refresh token is org-agnostic.ade auth login --org Xon an already-authenticated target switches in place, keeping login's guarantee semantics.x-org-idon every parse/extract request and survives token refreshes. API keys send no header — they are already organization-bound.ade help credentialstopic updated.Why the header, not Logto organization tokens
Logto supports org-bound tokens (
organization_idat the token grant, membership enforced at mint, org in a signed claim). That is the better end state, but platform authz does not read the claim yet, so it would need a server change to ship. The header needs none:checkAuthHeaderroutes JWT-shaped bearers throughverifyLogtoAccessToken(JWKS signature,iss,aud,typ: at+jwt), then resolves the org fromx-org-idviafetchOrgContextForUser, which hard-verifies membership (403 otherwise) and gates on that org's subscription and credit balance.The two approaches share all the CLI work and differ only in transport, so this does not paint us into a corner. ADR-0009 records the flip condition: when authz reads the
organization_idclaim, swap the header for the grant parameter — nothing user-visible changes.Live verification (dev)
Same user, same OIDC token, only the header changed:
ctplkf4kvkjshpblrpj3suw4parse-01kzd2cwqt9m5wsjr24rfz2j8b)cxwau16ra4zhparse-01kzd2d8zkx84g37xpmhgmebdv)Two fresh, uncached parses billed to two different organizations. Also exercised live: scope acceptance by dev Logto, userinfo discovery (3 real memberships), the ambiguous-name guard (two identically-named orgs), switch by id,
login --orgpreselect, and the non-interactive deferral note.Notes for reviewers
apps/authzin vision-agent-ui, currently compare-only) ignoresx-org-id. No runtime effect today, but every org-header request will show as a comparison divergence, and it must be ported before that service becomes the enforcer. Flagged in ADR-0009 — worth a heads-up to the platform team.Test plan
--orgmatching and the ambiguity guard, guarantee switch, header on submit and poll, refresh preservation, api-key isolation).ruff checkanduvx ty check srcclean;docs/reference/help.jsonregenerated.🤖 Generated with Claude Code