Repository navigation
feat: share one Ory login session between S3 credentials and the source.coop API - #23
Merged
Merged
Conversation
`login` now asks Ory for an access token meant for the source.coop API (`--audience`, default the site URL) and caches the whole session — refresh, ID and access tokens — once, rather than a refresh token per role. `creds` mints for any role by exchanging the session's ID token at `/.sts`, and `source-coop auth token` prints the access token, so the same login works for S3 and for the API. The refresh token lives only in the session, since it rotates on use and a replayed one can revoke the whole family: everything refreshes through `session::token`, under one lock, and one refresh renews both the ID and the access token. A role's cache keeps only the STS settings `login` was given. Caches from earlier versions, with a refresh token of the role's own, still refresh that way until the next `login`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF
alukach
marked this pull request as ready for review
October 9, 2026 19:37
alukach
added a commit
to source-cooperative/source.coop
that referenced
this pull request
Oct 13, 2026
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/v1` as the person who logged in. `getApiSession` routes a bearer by its unverified `iss`: Ory's tokens go to the new `authenticateWithOryAccessToken`, 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 is `NEXT_PUBLIC_ORY_SDK_URL`, which is each Ory project's issuer domain. **Deploying:** The CLI's Ory clients need the `jwt` access-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. | | Staging client (`a79c9537…`) | Production client (`197e20e7…`) | |---|---|---| | Allowed audience | `https://staging.source.coop` ✅ | `https://source.coop` ✅ | | `access_token_strategy: jwt` | ✅ set 2026-10-13 | ✅ set 2026-10-13 | **Changing a client** (done for both; kept for reference and rollback): `ory patch oauth2-client` doesn't exist, and `ory update oauth2-client` replaces the whole client. So export the client, change one field, and send the whole thing back: ```bash P=d567800d-b705-49d1-8a28-6f92f3a4c053 C=197e20e7-d52d-4d1d-9e54-4b73a342034b ory get oauth2-client $C --project $P --format json > prod-cli-client.before.json jq '.access_token_strategy = "jwt"' prod-cli-client.before.json > prod-cli-client.jwt.json ory update oauth2-client $C --project $P --file prod-cli-client.jwt.json ory get oauth2-client $C --project $P --format json | jq .access_token_strategy # "jwt" ``` Rollback: `ory update oauth2-client $C --project $P --file prod-cli-client.before.json`. - **No Vercel changes.** Each deployment's `NEXT_PUBLIC_ORY_SDK_URL` is already the issuer in its tokens: `https://auth.staging.source.coop` on staging and previews (checked against a real staging token), `https://auth.source.coop` on production. A local API can't accept CLI tokens at all, since no CLI client allows a localhost audience. - Merging deploys staging. Check with a staging CLI build: `source-coop login`, then `source-coop api whoami --api-url https://staging.source.coop` should return your account, and `creds` with `aws s3 ls` should still work. - The next release deploys production. Repeat the checks against `https://source.coop`. The release ships everything on `main`: if #651 is in it, data.source.coop#253 must be deployed first. - Rollback is a revert: only Ory-token calls are affected. **Docs:** - **ADR-005:** amended in source-cooperative/data.source.coop#254. It said the API authenticates proxy-signed tokens; it now records the second signer. - **OpenAPI:** the `bearerAuth` description names `source-coop login` and `source-coop auth token`. - **`public/llms.txt`:** checked. Its line ("a token from the CLI's login or a service account") still holds. - **docs.source.coop:** checked. No page describes calling the API with a token yet. ## Testing - `npm run type-check` passes. - `npx jest src/app/api src/lib`: 535 passed across 53 suites, including new suites for the Ory verifier and for `getApiSession`'s routing. - `next lint` on the changed files reports nothing. - Against real Ory (staging, 2026-10-13): after the CLI client was switched to `jwt`, a `source-coop login` token was an RS256 JWT with `iss` `https://auth.staging.source.coop` and `aud` `["https://staging.source.coop"]`, and empty `ext`. That matches what the verifier checks. The API call itself (`source-coop api whoami` against staging) waits for this to deploy, since a preview's origin isn't an allowed audience. ## Related - CLI login session, which sends these tokens: source-cooperative/source-coop-cli#23 - CLI product commands: source-cooperative/source-coop-cli#24 - ADR-005 amendment: source-cooperative/data.source.coop#254 - Deployment runbook: in source-cooperative/source-coop-cli#23 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
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 #21. This is the auth groundwork for the gh-style commands: one
loginnow works for both S3 and the source.coop API.loginasks Ory for an access token whose audience is the source.coop site (--audience/SOURCE_API_AUDIENCE, defaulthttps://source.coop, orhttps://staging.source.coopin staging builds). It caches the whole Ory session (refresh, ID and access tokens) once, under@sessionin the keyring orsource-coop/session.json, rather than a refresh token per role.credsmints for any role, including one never logged into, by exchanging the session's ID token at/.sts. The newsource-coop auth tokenprints the access token, refreshed if it has expired.The refresh token lives only in the session. It rotates on use, and a replayed one can revoke the whole token family, so every role and every API call refreshes through one function under one lock, and one refresh renews both tokens. A role's cache keeps only its STS settings. Caches written by 0.3.0 still refresh with their own token until the next
login, so nobody has to log in again on upgrade. 0.3.0 reading a cache written by this version serves the credentials until they expire, then says to log in.Deploying: the CLI's Ory clients must allow the site as an audience and use the
jwtaccess-token strategy before this is released, orloginis refused. See the runbook below, steps 1, 2 and 6.Testing
cargo fmt --check,cargo clippy --all-targets -- -D warningsandcargo test(28 passed) all pass./.stsand the API, with an expired session cached:auth tokenand API calls sent the new access token.credsminted for a role with nothing cached, using the ID token that same refresh brought.Deployment runbook (all four PRs)
Staging goes first at every step, and production only after staging checks out. Nothing here is hard to undo; each phase lists its rollback.
0. Get the branches onto GitHub
Done when you're reading this:
open-prs.shopened these four drafts, linked them to each other, and put the source.coop PR's number into ADR-005's note.1. Configure the CLI's Ory OAuth2 clients (staging, then production)
The CLI's clients are
a79c9537-be78-454a-9ea1-b96a1be811cc(staging) and197e20e7-d52d-4d1d-9e54-4b73a342034b(production). Change only these clients, not the project and not the web app's client.ory get oauth2-client <client-id> --project <project> --format json > cli-client-before.jsonory update oauth2-clientreplaces the whole client and drops any field you leave out. Check the flags withory patch oauth2-client --helpfor your CLI version:https://staging.source.coopon the staging client,https://source.coopon the production client.access_token_strategytojwt.offline_accessis among its scopes andrefresh_tokenamong its grant types. 0.3.0's refresh already relies on both, so they should be there.source-coop loginandsource-coop credswith the installed 0.3.0. It never asks for an audience, and its ID tokens are unaffected by the access-token strategy.Rollback: restore
cli-client-before.jsonwithory update oauth2-client <client-id> --file cli-client-before.json. A full replace is what you want here.2. Read a real token's issuer (staging)
The API matches
issexactly, with or without a trailing slash, so check what Ory actually puts there before deploying the API change.git checkout feat/login-session && cargo build --features staging.jwtstrategy from step 1 isn't in effect.audcontainshttps://staging.source.coop. If it doesn't, the consent step didn't grant the audience.issequals staging'sNEXT_PUBLIC_ORY_SDK_URL, with or without a trailing slash. If it doesn't, setORY_OAUTH2_ISSUERto the token'sissin Vercel's staging environment.3. Deploy the API change to staging
feat/api-ory-access-tokens). Merging tomaindeploys staging.curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer <an ID token>" https://staging.source.coop/api/v1/whoamiThis should not resolve to your account.
Rollback: revert the merge. Ory-token calls then get 401, and nothing else changes: the proxy, the web app and S3 credentials don't use this path.
4. Check the product commands on staging
These need source.coop#651 deployed to staging, and #651 needs data.source.coop#253 deployed first (#651's own deploy-order note).
git checkout feat/product-commands && cargo build --features stagingcredsafter the cached credentials expire. It should refresh without a browser.5. Production
cargo build, no features), as in step 2. SetORY_OAUTH2_ISSUERin Vercel's production environment ifissdiffers.https://source.coop.6. Release the CLI
run_credsand the cache. Whichever merges second rebases. Whenfeat/login-sessionrebases on feat: exchange a service account's API key without a browser #20, minting from the session should take feat: exchange a service account's API key without a browser #20's early-refresh rule (16 minutes left, or half a shorter session).feat/login-session, thenfeat/product-commands. GitHub retargets the second tomainonce the first's branch is deleted.feat:commits make it 0.4.0. Merging it tags the release, and cargo-dist builds the installers and publishes the Homebrew formula.brew upgrade source-coopor the installer script) and runsource-coop login,source-coop auth token,source-coop api whoamiandsource-coop product list.product createandsource-coop api(not part of these PRs).Rollback: the previous release still works against everything above. Users on the new release who hit login trouble can pass
--audience '', which logs in as 0.3.0 did, without API commands.Notes
--audience <preview origin> --api-url <preview origin>.Related
🤖 Generated with Claude Code
https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF