Google OIDC provider: link-only, sub-keyed, PKCE and nonce #2
Loading…
Reference in a new issue
No description provided.
Delete branch "heimdall/google-oidc-provider"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
googleas a Heimdall OpenID Connect provider, pergamecult-chat:ruling:google-first-path-ruled(Google behind Heimdall; Bifrost has no direct Google upstream). Not deployed, not merged, no secret touched.Callback redirect URI (register exactly this in the Google OAuth client):
https://heimdall.gamecult.org/v1/oauth/google/callbackIt is
{GC_ACCESS_BASE_URL}/v1/oauth/google/callback; production's value is taken fromdocs/architecture.md, not from the live host.What it does
openid email profile;state(Heimdall-signed),nonce, PKCES256.src/google-oidc.tsowns the decision: RS256 only, signature checked against Google's published keys before any claim is read;issGoogle,audthis client (an arrayaudalso needsazp),exp/iat/nbfwith 60 s leeway,nonceequal to this request's,subpresent,emailpresent andemail_verified.sub. The email is a display/contact field and is never a lookup key.jtiand Heimdall's Ed25519 signing key (deterministic signature), so nothing is stored between start and callback and a reader of the redirect cannot compute them.Linking (settled)
firstLogin: "requires_link"on the descriptor).mode: linkneedsAuthorization: Bearer <Heimdall access token>for the same app (verified signature, issuer, audience, window, and a live session at its revision). The identity attaches to that account only.mode: sign_insigns in an account thesubis already linked to. An unknownsubis refused and nothing is minted; asubheld by another account is refused and never moved.bifrostprofile only (profile version bumped). The Idunn recipe declaresGC_ACCESS_PROVIDER_GOOGLE_CLIENT_IDand..._CLIENT_SECRET_FILE.Tests
tests/google-oidc.test.ts(33) andtests/google-link.test.ts(14), including PKCE proven end to end (the verifier sent hashes to the challenge in the URL), nonce replay, wrong audience, unverified email, expired token, rotated keys, a stranger'ssub, and asublinked elsewhere. Mutation check by hand: removing the no-mint guard, the linked-elsewhere check, theemail_verifiedcheck, the verifier or the nonce check each fails a test.Full suite: 19 failed / 432 passed / 146 skipped. The same 19 fail on
mainat5146c72before this change: 13 test files cannot load@gamecult/cultcache-ts/cultnet-tsbecausevendor/CultLibpackages are not built in my workspace (ERR_MODULE_NOT_FOUND ... dist/index.js). Typecheck is clean apart from those module errors.Known gaps, do not enable
linkin production until these are settledentitlement.app_accessfrom a linked Discord/Patreon identity appears only after a refresh with policies. Bifrost must refresh after a Google sign-in or Heimdall must evaluate all linked identities at the callback.oauth_callback_failed; the audit event carriesAccountLinkRefusedand the code.🤖 Generated with Claude Code
Pushed
c829429: both follow-ups fixed.google-link-two-phase. The callback no longer writes a link. It records a pending link (
pending_links, TTL =completionTtlSeconds, single use, store-minted id) and answersstatus: link_pendingwithpendingLinkIdand no session. OnlyPOST /v1/apps/:appSlug/links/confirmwith the Heimdall access token of the account that began the link writes the identity. A different account, expired, used or unknown id gives 410; asubanother account holds by then gives 409; a refused confirm does not burn the link. Applies to any provider started withmode: link. Test: a victim completing an attacker's link URL attaches the victim's sub to no account.google-linked-entitlements-at-callback. Every callback now also evaluates the account's other linked identities with the start-time entitlement policy (the refresh route's call), so a Discord-origin member returning via Google keeps
entitlement.app_access. Without a start-time policy they grant nothing.Tests: 22 in google-link (8 new), mutation-checked (dropping the account bind or the linked-facts merge each fails a test). Full suite: only the two failures that were already there (
caller-identity-attack-soul-r21startOAuthFlow callers;verse-runtimeproviders=2/7 vs 2/8). Postgres tests skipped here, no database:pending_linksschema andstore.test.tscounts (95 columns, 13 keys) are unrun against Postgres.Still open: the Bifrost side. Bifrost has to call confirm with the user's token (and the pending link id from the callback result), and needs a link action. For a
backend_callbackhandoff thelink_pendingpayload goes to the app keyed by attemptId, so Bifrost must only confirm for the browser that holds that attempt's cookie; otherwise an attacker's attempt can still receive the victim's pending id. Not deployed, no hosts, no secrets.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.