Skip to content

feat: discovery-driven OIDC auth with device code & refresh token flows#5

Draft
alukach wants to merge 23 commits into
mainfrom
feat/oidc-based-auth
Draft

feat: discovery-driven OIDC auth with device code & refresh token flows#5
alukach wants to merge 23 commits into
mainfrom
feat/oidc-based-auth

Conversation

@alukach

@alukach alukach commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What this works toward

Replaces the CLI's single hardcoded auth flow (Authorization Code + PKCE) with a discovery-driven OIDC system for Source Cooperative. The CLI now reads the provider's OIDC discovery document and selects the best available flow, and AWS credentials refresh themselves silently.

Design doc: docs/plans/2026-03-12-oidc-discovery-auth-design.md

Changes

OIDC discovery drives flow selection

  • Fetches .well-known/openid-configuration and reads real capabilities (endpoints, grant_types_supported) instead of hardcoding.
  • oidc.rs is restructured into an oidc/ module.

Three auth flows instead of one

  • Authorization Code + PKCE — existing flow, moved to auth_code.rs; needs a browser on the same machine.
  • Device Code (RFC 8628) — new; works headless / over SSH.
  • Refresh Token — new; silent re-auth.
  • Selection priority: --flow flag > device-code (if supported) > auth-code. Each flow returns an ID token, then the unchanged STS step performs AssumeRoleWithWebIdentity.

Refresh tokens cached + auto-refresh

  • Login requests offline_access scope and stores the refresh token in a separate keyring entry (file fallback, keyed by issuer).
  • source-coop creds silently exchanges the refresh token for a new ID token → new AWS creds when the cached creds are expired. --no-refresh opts out.

New logout command

  • Revokes the refresh token via the revocation endpoint and clears both cached AWS creds and the refresh token.

Result

One CLI that works on a laptop or a headless box, with credentials that quietly renew until the refresh token is revoked.

Status

Draft — still wiring up / debugging (recent commits add proxy_url/role_arn to refresh data and debug logging).

🤖 Generated with Claude Code

alukach and others added 21 commits March 12, 2026 12:20
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…delete error handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…letes

- Drop unused OidcDiscovery/TokenResponse fields and the FlowType::Auto
  variant (it always resolved to AuthCode); default --flow is now auth-code.
- Remove verbose JWT-claims decode block from run_login.
- Inline issuer_key and merge delete_refresh_token/delete_credentials into
  a shared delete_entry helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The committed staging CLIENT_ID (c445cc61…) does not exist in the staging
Ory project; auth.staging.source.coop returns invalid_client for it. The real
source-coop-cli client is a79c9537-be78-454a-9ea1-b96a1be811cc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Source it (`source .env.staging`) to override issuer/client/proxy via the
existing SOURCE_* env vars — no rebuild needed. Values are public (OIDC public
client + public URLs). Ignore other .env files to avoid committing secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Device-code login is blocked: Ory Network silently drops the
urls.device.verification/success project-config keys, so Hydra can't be
pointed at a custom verification UI. Captures what works, how it fails, root
cause, and the ask.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@saulbert

Copy link
Copy Markdown

Symptom: with #6's corrected prod client (197e20e7…), source-coop login reaches the Ory consent page, but clicking Allow spins forever — the auth-code callback never fires and the CLI hangs.

Clues:

  • consent page console: Uncaught SyntaxError: Identifier 'togglePassword' has already been declaredpasswordInput.js loads twice.
  • reproduces in clean incognito, all on one host (not a loopback issue).
  • 197e20e7… itself is fine — confirmed live on auth.source.coop.

Best guess (low confidence): the double-loaded passwordInput.js throws on parse and takes down the bundle holding Allow's submit handler, so consent never POSTs.

This PR's device-code flow would sidestep the browser consent + loopback entirely (also ideal for headless/CI), so it looks like the clean unblock. Does docs/ory-network-device-flow-limitation.md affect that path?

Happy to test against catalog publishing — just point at a branch. Thanks for the work here.

[this comment written by Claude Opus 4.8 and approved by @saulpw]

@alukach

alukach commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @saulbert. I'm not familiar with a "spins forever" bug. #6 has since been merged, I recommend you try again off of main. Please open an issue if you run into any issues.

I would like to get this PR working but for the reasons described in docs/ory-network-device-flow-limitation.md (pasted below), it appears that Device Flow is not supported on Ory Network.


Ory Network: urls.device.verification / urls.device.success silently dropped — device flow unusable

Environment: Ory Network (managed). OAuth2 device authorization grant (RFC 8628).

What works

  1. The grant + code issuance. Adding the device grant to an OAuth2 client
    succeeds, and the device-authorization endpoint issues codes:

    ory update oauth2-client <CLIENT_ID> \
      --grant-type authorization_code --grant-type refresh_token \
      --grant-type urn:ietf:params:oauth:grant-type:device_code \
      --token-endpoint-auth-method none …
    
    curl -X POST https://<PROJECT_SLUG>.projects.oryapis.com/oauth2/device/auth \
      -d client_id=<CLIENT_ID> -d 'scope=openid offline_access'
    # → returns user_code, device_code, verification_uri, verification_uri_complete ✓
  2. The verification mechanism itself. A self-hosted verification page would
    work fully. Taking the device_challenge that Hydra puts in the fallback URL
    and accepting the code via the admin API returns 200 with a redirect_to
    that continues into the normal login/consent flow:

    curl -X PUT "https://<PROJECT_SLUG>.projects.oryapis.com/admin/oauth2/auth/requests/device/accept?device_challenge=<CHALLENGE>" \
      -H "Authorization: Bearer <PROJECT_API_KEY>" -H "Content-Type: application/json" \
      -d '{"user_code":"<USER_CODE>"}'
    # → 200 { "redirect_to": ".../oauth2/device/verify?client_id=…&device_verifier=…&user_code=…" }

    So every step is functional — issuance, code entry, accept, and hand-off to
    login/consent. The only missing link is getting Hydra to send the browser
    to a custom verification page in the first place.

What fails

Setting the device verification/success UI URLs is silently discarded:

ory patch project <PROJECT_SLUG> \
  --add '/services/oauth2/config/urls/device={"verification":"https://app.example.com/device","success":"https://app.example.com/device/success"}'
# → "Project updated successfully!"  (no error, device NOT in the ignored-keys warning)

Two ways to confirm it didn't persist:

# 1. Read-back: device key is absent
ory get project <PROJECT_SLUG> --format json | jq '.services.oauth2.config.urls'
# → { consent, error, login, logout, post_logout_redirect, registration, self }   ← no "device"

# 2. Live flow still hits the built-in fallback
#   following verification_uri_complete 302-redirects to:
#   https://<PROJECT_SLUG>.projects.oryapis.com/oauth2/fallbacks/device?device_challenge=…

Root cause

Ory Network persists Hydra config as a flat normalizedProjectRevision, which
has no field for urls.device.verification / urls.device.success (only
login/consent/logout/error/registration/post_logout_redirect/self exist).
Self-hosted Hydra's config schema defines these keys; the managed Network schema
does not expose them, so the API accepts the patch and drops the key. Confirmed
across ory patch project, ory patch oauth2-config, and ory update — all
normalize into the same schema.

Impact

With urls.device.verification unset, Hydra routes the user to
/oauth2/fallbacks/device, a hardcoded static error page ("configuration
key urls.device.verification is not set") — no code-entry form. Since the
verification mechanism itself works (see above), this is purely a routing gap:
there is no way to point the browser at a working verification UI. The user can
never approve the device, so the client's token poll never completes and device
login hangs until the code expires.

Ask

Expose urls.device.verification / urls.device.success in the Ory Network
project config schema (the Hydra device grant shipped in v25.4.0, but these URL
keys aren't settable on Network), or document the supported way to point the
device flow at a custom verification UI.

References

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.

2 participants