Link a Google login to a signed-in member, explicit link only; Bifrost stores no Heimdall token #81

Merged
heimdall merged 3 commits from heimdall/google-link into main 2026-11-12 16:20:50 +00:00
Member

Steps 1-5 of mind follow-up bifrost-google-link-plan, under the steward's rulings bifrost-google-link-bearer-fresh-signin and bifrost-google-invitation-link-only.

Flow (a state machine over the existing protected attempt cookie, which carries a purpose, the expected Heimdall account id and a pending link id, never a token):

  1. Signed-in member POSTs Link Google on /Membership/Status (antiforgery) -> ordinary Heimdall sign-in, attempt LinkStart.
  2. Completion: account must equal the session's; the freshly redeemed bearer is used in memory to POST /v1/oauth/google/start mode=link; browser goes to Google via Heimdall (LinkAwaitGoogle).
  3. Return with heimdall_pending_link_id (the complete page now forwards it) -> a second fresh Heimdall sign-in (LinkConfirm).
  4. Completion: fresh bearer -> POST /links/confirm; membership refreshed from the verified identity; session untouched; fixed outcome wording for linked / identity_linked_elsewhere (409) / expired (410) / error.

Google stays out of sign-in and member access. Old attempt cookies without a purpose still read as sign-in. The Authorization header is now redacted from HttpClient logs (a bearer would have logged at Trace).

Notes: no /account page exists, so the button is on /Membership/Status. Replaying step C starts another fresh sign-in but grants nothing (pending id is single-use at Heimdall); strict single-use would need server-side consumed-nonce state, not added. The pending link id sits in the LinkConfirm cookie by design.

Tests first (red 16/30 before code); Heimdall-filtered 185 pass; mutations on no-token-in-cookie, account match, session match, antiforgery, header redaction all caught. Not deployed. Needs a Google client configured in Heimdall before it can work live; Eureka decides the ship.

🤖 Generated with Claude Code

Steps 1-5 of mind follow-up `bifrost-google-link-plan`, under the steward's rulings `bifrost-google-link-bearer-fresh-signin` and `bifrost-google-invitation-link-only`. **Flow** (a state machine over the existing protected attempt cookie, which carries a purpose, the expected Heimdall account id and a pending link id, never a token): 1. Signed-in member POSTs `Link Google` on `/Membership/Status` (antiforgery) -> ordinary Heimdall sign-in, attempt `LinkStart`. 2. Completion: account must equal the session's; the freshly redeemed bearer is used in memory to `POST /v1/oauth/google/start mode=link`; browser goes to Google via Heimdall (`LinkAwaitGoogle`). 3. Return with `heimdall_pending_link_id` (the complete page now forwards it) -> a second fresh Heimdall sign-in (`LinkConfirm`). 4. Completion: fresh bearer -> `POST /links/confirm`; membership refreshed from the verified identity; session untouched; fixed outcome wording for linked / identity_linked_elsewhere (409) / expired (410) / error. Google stays out of sign-in and member access. Old attempt cookies without a purpose still read as sign-in. The `Authorization` header is now redacted from HttpClient logs (a bearer would have logged at Trace). **Notes:** no `/account` page exists, so the button is on `/Membership/Status`. Replaying step C starts another fresh sign-in but grants nothing (pending id is single-use at Heimdall); strict single-use would need server-side consumed-nonce state, not added. The pending link id sits in the LinkConfirm cookie by design. Tests first (red 16/30 before code); Heimdall-filtered 185 pass; mutations on no-token-in-cookie, account match, session match, antiforgery, header redaction all caught. Not deployed. Needs a Google client configured in Heimdall before it can work live; Eureka decides the ship. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The account page's POST starts an ordinary sign-in whose attempt (a purpose, the expected
account id, a pending link id; never a token) is only the proof for a link. Each Heimdall
call that needs a bearer follows its own fresh sign-in; the bearer lives in that request.
Google stays out of AllowedProviders and member access. Authorization is redacted from
HttpClient logging. Outcomes redirect to a fixed closed set of wording.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost!81
No description provided.