Skip to content

feat: share one Ory login session between S3 credentials and the source.coop API - #23

Merged
alukach merged 1 commit into
mainfrom
feat/login-session
Oct 9, 2026
Merged

alukach merged 1 commit into
mainfrom
feat/login-session

Conversation

@alukach

@alukach alukach commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Part of #21. This is the auth groundwork for the gh-style commands: one login now works for both S3 and the source.coop API.

login asks Ory for an access token whose audience is the source.coop site (--audience / SOURCE_API_AUDIENCE, default https://source.coop, or https://staging.source.coop in staging builds). It caches the whole Ory session (refresh, ID and access tokens) once, under @session in the keyring or source-coop/session.json, rather than a refresh token per role. creds mints for any role, including one never logged into, by exchanging the session's ID token at /.sts. The new source-coop auth token prints 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 jwt access-token strategy before this is released, or login is refused. See the runbook below, steps 1, 2 and 6.

Testing

  • cargo fmt --check, cargo clippy --all-targets -- -D warnings and cargo test (28 passed) all pass.
  • The session tests cover a fresh token used as is, an expired access or ID token refreshing and rotating, the rotated token being saved before use, no access token without an audience, and an expired session with no refresh token saying to log in.
  • End to end, I ran the binary against a local stand-in for Ory, /.sts and the API, with an expired session cached:
    • One refresh rotated the refresh token.
    • auth token and API calls sent the new access token.
    • creds minted for a role with nothing cached, using the ID token that same refresh brought.
  • Not run against real Ory: the client configuration in runbook step 1 doesn't exist yet.

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.sh opened 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) and 197e20e7-d52d-4d1d-9e54-4b73a342034b (production). Change only these clients, not the project and not the web app's client.

  1. Back up the client before changing it:
    ory get oauth2-client <client-id> --project <project> --format json > cli-client-before.json
  2. Patch it rather than updating it. ory update oauth2-client replaces the whole client and drops any field you leave out. Check the flags with ory patch oauth2-client --help for your CLI version:
    • Add the site to its allowed audiences: https://staging.source.coop on the staging client, https://source.coop on the production client.
    • Set access_token_strategy to jwt.
    • Confirm offline_access is among its scopes and refresh_token among its grant types. 0.3.0's refresh already relies on both, so they should be there.
  3. Check that the current release still works: run source-coop login and source-coop creds with 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.json with ory 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 iss exactly, with or without a trailing slash, so check what Ory actually puts there before deploying the API change.

  1. Build the CLI branch for staging: git checkout feat/login-session && cargo build --features staging.
  2. Log in and print the token's claims:
    ./target/debug/source-coop login
    ./target/debug/source-coop auth token | python3 -c 'import sys,json,base64;p=sys.stdin.read().split(".")[1];print(json.dumps(json.loads(base64.urlsafe_b64decode(p+"="*-len(p)%4)),indent=2))'
  3. Check that:
    • The token has three dot-separated parts. If it doesn't, it's opaque: the jwt strategy from step 1 isn't in effect.
    • aud contains https://staging.source.coop. If it doesn't, the consent step didn't grant the audience.
    • iss equals staging's NEXT_PUBLIC_ORY_SDK_URL, with or without a trailing slash. If it doesn't, set ORY_OAUTH2_ISSUER to the token's iss in Vercel's staging environment.

3. Deploy the API change to staging

  1. Merge the source.coop PR (feat/api-ory-access-tokens). Merging to main deploys staging.
  2. Merge the data.source.coop ADR PR. It's documentation only.
  3. Check it with the staging build from step 2:
    ./target/debug/source-coop api whoami --api-url https://staging.source.coop   # your account, not a 401
    ./target/debug/source-coop creds | jq .Expiration                             # S3 still works
    aws s3 ls s3://<your-account>/ --profile <staging profile>
  4. Check that an ID token is refused:
    curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer <an ID token>" https://staging.source.coop/api/v1/whoami
    This 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).

  1. git checkout feat/product-commands && cargo build --features staging
  2. Use a test account you own:
    B=./target/debug/source-coop; export SOURCE_API_URL=https://staging.source.coop
    $B product list <account>                      # table on a terminal
    $B product list <account> | cat                # tab-separated
    $B product create <account>/cli-smoke-test     # prompts; try a bad ID to see it asked again
    $B product edit <account>/cli-smoke-test       # field picker; `e` opens your editor
    $B product view <account>/cli-smoke-test --web
    $B product delete <account>/cli-smoke-test     # asks about data, then for the name
  3. Check that an upgrade needs no new login: with a credentials cache written by 0.3.0, run the new build's creds after the cached credentials expire. It should refresh without a browser.

5. Production

  1. Patch the production CLI client (step 1), then read a production token's claims with a default build (cargo build, no features), as in step 2. Set ORY_OAUTH2_ISSUER in Vercel's production environment if iss differs.
  2. Merge source.coop's release-please PR. Publishing the release deploys production; it carries this change and #651.
  3. Repeat step 3's checks against https://source.coop.

6. Release the CLI

  1. Settle the order with source-coop-cli#20 (API-key exchange), since both rewrite run_creds and the cache. Whichever merges second rebases. When feat/login-session rebases 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).
  2. Merge feat/login-session, then feat/product-commands. GitHub retargets the second to main once the first's branch is deleted.
  3. release-please opens a release PR; feat: commits make it 0.4.0. Merging it tags the release, and cargo-dist builds the installers and publishes the Homebrew formula.
  4. Install the release (brew upgrade source-coop or the installer script) and run source-coop login, source-coop auth token, source-coop api whoami and source-coop product list.
  5. Follow up on docs.source.coop: the CLI page can now cover product create and source-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

  • Preview deployments: a preview's origin isn't an allowed audience, so the CLI's API commands can't sign in against one. Test those on staging, or add the preview's origin to the staging client's audiences and run the CLI with --audience <preview origin> --api-url <preview origin>.
  • Older releases: a 0.3.0 install isn't affected by any of this. It never calls the API, and its cached refresh tokens keep working.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_017poyHYEyxWEJct9X8Q71qF

`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 alukach changed the title … feat: share one Ory login session between S3 credentials and the source.coop API Oct 9, 2026
@alukach
alukach added this pull request to stack #25 October 9, 2026 19:20
@alukach
alukach marked this pull request as ready for review October 9, 2026 19:37
@alukach
alukach merged commit d55e4b7 into main Oct 9, 2026
9 checks passed
@alukach
alukach deleted the feat/login-session branch October 9, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant