Repository navigation
feat(api): accept Ory access tokens from the CLI's login - #654
Merged
Merged
Conversation
The API now takes an Ory access token as a bearer, alongside the assertions the data proxy signs. `source-coop login` asks Ory for an access token whose audience is the site's origin, and `getApiSession` routes a bearer by its unverified `iss`: Ory's go to a new verifier, the proxy's to the existing one, and neither falls back to the other or to the cookie. The Ory verifier checks the token against Ory's published keys (the CLI's client uses the `jwt` access-token strategy), with the issuer, RS256, expiry and the request's origin as audience, so an ID token, which Ory issues to the client ID, never passes. The subject is looked up only as a person's Ory identity. Both verifiers now build the session in one place, `sessionForAccount`, so a token acts with the same rights whichever way it arrived. `ORY_OAUTH2_ISSUER` names Ory's issuer when it isn't the SDK URL. Part of #588. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Claude finished @alukach's task in 24s —— View job ✅ No blocking issues — safe to merge. I read the verifier, the routing in
Non-blocking notes:
Simplify (ponytail)
DocsThe description covers all three: the ADR-005 amendment is linked (data.source.coop#254), the 💰 Estimated review cost: $0.11 · 0m23s · 4 turns |
alukach
added a commit
that referenced
this pull request
Oct 13, 2026
…661) Part of #588. ## What The API stops accepting the data proxy's signed assertions for any write. `getApiSession` returns no session for a bearer on anything but `GET` or `HEAD`, and doesn't fall back to the cookie. The proxy can sign an assertion for any subject (ADR-005), so as things stand its signing key can create, edit and delete products, data connections, memberships and accounts as anyone. Nothing needs that. The proxy sends only reads: - `GET` lookups of products, permissions, data connections and product lists, as the caller (`data.source.coop` `src/source_api/cache.rs`). - `POST /service-account-keys/exchanges`, as itself. This route already verifies the assertion itself and doesn't use `getApiSession`, so it's unaffected. - `POST /accounts/{account_id}/trusts/exchanges`, as the account named in `RoleArn`. This is a lookup sent as a `POST`. It now verifies the assertion with `authenticateWithOidcToken` directly, so it keeps working and accepts the same callers as before: the account itself or an admin. A browser cookie no longer reaches it, which nothing used. An org-wide search found no other bearer caller of a mutating `/api/v1` route. `metadata-catalog-pipeline` and ops-source's crawler only `GET`. ## With #654 #654 routes a bearer to the Ory or proxy verifier by its `iss`. When it merges after this, the method check has to move into the proxy branch, so the CLI's Ory access tokens can still write. Whichever PR merges second carries that change. ## Docs - ADR-005 says the API trusts the proxy to assert any `sub`. Once this merges, it should also say those assertions only read. That belongs in a follow-up PR to data.source.coop alongside data.source.coop#254. - No OpenAPI or `llms.txt` change: no route's documented behaviour changes for any real caller. ## Testing - `npx jest`: 88 suites, 905 tests pass. - `npm run type-check` passes. - `npm run lint`: no new warnings. - New: `src/lib/api/get-api-session.test.ts` checks that a bearer still reads on `GET` and `HEAD`, and gets no session (and is never verified) on `POST`, `PUT`, `PATCH` and `DELETE`. The trust exchange tests now mock the verifier the route calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…okens # Conflicts: # src/lib/api/get-api-session.test.ts
Every deployment's NEXT_PUBLIC_ORY_SDK_URL is its Ory project's issuer, and the one case the override was for, a local API, can't get a token anyway: no CLI client allows a localhost audience. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r directly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully 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.
Part of #588. The CLI half is source-cooperative/source-coop-cli#23.
The API now takes an Ory access token as a bearer, alongside the assertions the data proxy signs, so the CLI can call
/api/v1as the person who logged in.getApiSessionroutes a bearer by its unverifiediss: Ory's tokens go to the newauthenticateWithOryAccessToken, and the proxy's go to the existing verifier. Neither falls back to the other or to the session cookie.The Ory verifier checks the token against Ory's published JWKS, with the issuer (with or without a trailing slash), RS256, expiry, a 30-second tolerance, and the request's origin as audience. An ID token, which Ory issues to the client ID, never passes, and which clients may mint a token for the API is decided in Ory by the audiences each client is allowed. The subject is looked up only as a person's Ory identity. Both verifiers build the session in one place,
sessionForAccount. The issuer isNEXT_PUBLIC_ORY_SDK_URL, which is each Ory project's issuer domain.Deploying:
The CLI's Ory clients need the
jwtaccess-token strategy and this site's origin as an allowed audience. Until a client has both, the API answers that environment's Ory tokens with 401. Nothing else breaks: only newer CLI builds send Ory tokens, so merging first is safe.a79c9537…)197e20e7…)https://staging.source.coop✅https://source.coop✅access_token_strategy: jwtChanging a client (done for both; kept for reference and rollback):
ory patch oauth2-clientdoesn't exist, andory update oauth2-clientreplaces the whole client. So export the client, change one field, and send the whole thing back:Rollback:
ory update oauth2-client $C --project $P --file prod-cli-client.before.json.NEXT_PUBLIC_ORY_SDK_URLis already the issuer in its tokens:https://auth.staging.source.coopon staging and previews (checked against a real staging token),https://auth.source.coopon production. A local API can't accept CLI tokens at all, since no CLI client allows a localhost audience.source-coop login, thensource-coop api whoami --api-url https://staging.source.coopshould return your account, andcredswithaws s3 lsshould still work.https://source.coop. The release ships everything onmain: if feat(api): product list, create, view, edit and delete #651 is in it, data.source.coop#253 must be deployed first.Docs:
bearerAuthdescription namessource-coop loginandsource-coop auth token.public/llms.txt: checked. Its line ("a token from the CLI's login or a service account") still holds.Testing
npm run type-checkpasses.npx jest src/app/api src/lib: 535 passed across 53 suites, including new suites for the Ory verifier and forgetApiSession's routing.next linton the changed files reports nothing.jwt, asource-coop logintoken was an RS256 JWT withisshttps://auth.staging.source.coopandaud["https://staging.source.coop"], and emptyext. That matches what the verifier checks. The API call itself (source-coop api whoamiagainst staging) waits for this to deploy, since a preview's origin isn't an allowed audience.Related
🤖 Generated with Claude Code
https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF