The API now supports bidirectional account linking via a unified provider registry — landed in ag-tech-group/criticalbit-auth-api#40. The frontend needs three pieces of work to expose this to users.
New API surface
| Method |
Path |
Notes |
| GET |
/auth/{provider}/associate/authorize |
Authenticated. Starts the link flow. In dev returns {authorization_url} JSON; in prod 302s. Provider can be google or steam (Twitch / Battle.net / YouTube when those land). |
| GET |
/auth/{provider}/associate/callback |
Authenticated. Finishes the link. 302 to /callback/{provider}-associate-complete on success. 409 with {detail: {code: "oauth_account_already_linked"}} if the identity is already on another account. |
| GET |
/auth/me/connections |
Returns [{provider, account_id, account_email}] — what to render on the settings page. |
What the frontend needs
1. New /callback/{provider}-associate-complete landing page
After a successful associate-callback the API redirects here. The page should:
- Confirm the link succeeded (likely via
GET /auth/me/connections and showing the new entry).
- Return the user to the settings/profile page.
2. New /callback/steam page handling associate (in dev)
In dev mode the API points Steam's openid.return_to at ${FRONTEND_URL}/callback/steam — the same URL used for login. The frontend page already forwards login callbacks to the API; it now needs to ALSO forward associate callbacks. The way to tell them apart: decode the state JWT (no signature verification needed, just base64) and check the purpose field — "login" vs "associate". Forward to /auth/steam/callback or /auth/steam/associate/callback respectively. Same logic for Google's /callback/google.
3. "Connections" section on the account settings page
- Call
GET /auth/me/connections to see what's linked.
- Show a "Connect Google" / "Connect Steam" button for each provider not in the list.
- Show "Connected as
<account_email>" (or <account_id> for Steam) for each provider in the list.
- (Future PR 3) "Disconnect" buttons — coming when the API ships
DELETE /auth/me/connections/{provider}.
Error handling for the merge-refusal case
The associate-callback can return:
- 409
oauth_account_already_linked — surface the existing message: "This <Provider> account is already linked to another criticalbit account. Unlink it there first." Don't auto-retry.
- 400
oauth_state_invalid / oauth_state_expired / oauth_csrf_mismatch — session got stale; tell the user to retry. The "expired" case is the common one (state TTL is 1h).
- 400
oauth_state_user_mismatch — should not happen in normal flow; signals tampering. Surface generically and log.
- 400
oauth_verify_failed — provider rejected the response (e.g., Steam OpenID assertion failed). Generic retry.
Order of operations
You can ship the connections UI first (item 3, read-only listing) without items 1 and 2. The link flow itself is item 1 + 2 together — both are needed to complete an associate round-trip.
Related
- This issue replaces the manual "link Google" feature that's been live since before this refactor. The Google associate URL stays at
/auth/google/associate/authorize — same path as before — so any existing frontend code targeting it should still work.
- Disconnect UX waits on PR 3 (API DELETE endpoint +
User.has_usable_password column to enforce "must keep at least one login method").
The API now supports bidirectional account linking via a unified provider registry — landed in
ag-tech-group/criticalbit-auth-api#40. The frontend needs three pieces of work to expose this to users.New API surface
/auth/{provider}/associate/authorize{authorization_url}JSON; in prod 302s. Provider can begoogleorsteam(Twitch / Battle.net / YouTube when those land)./auth/{provider}/associate/callback/callback/{provider}-associate-completeon success. 409 with{detail: {code: "oauth_account_already_linked"}}if the identity is already on another account./auth/me/connections[{provider, account_id, account_email}]— what to render on the settings page.What the frontend needs
1. New
/callback/{provider}-associate-completelanding pageAfter a successful associate-callback the API redirects here. The page should:
GET /auth/me/connectionsand showing the new entry).2. New
/callback/steampage handling associate (in dev)In dev mode the API points Steam's
openid.return_toat${FRONTEND_URL}/callback/steam— the same URL used for login. The frontend page already forwards login callbacks to the API; it now needs to ALSO forward associate callbacks. The way to tell them apart: decode thestateJWT (no signature verification needed, just base64) and check thepurposefield —"login"vs"associate". Forward to/auth/steam/callbackor/auth/steam/associate/callbackrespectively. Same logic for Google's/callback/google.3. "Connections" section on the account settings page
GET /auth/me/connectionsto see what's linked.<account_email>" (or<account_id>for Steam) for each provider in the list.DELETE /auth/me/connections/{provider}.Error handling for the merge-refusal case
The associate-callback can return:
oauth_account_already_linked— surface the existing message: "This<Provider>account is already linked to another criticalbit account. Unlink it there first." Don't auto-retry.oauth_state_invalid/oauth_state_expired/oauth_csrf_mismatch— session got stale; tell the user to retry. The "expired" case is the common one (state TTL is 1h).oauth_state_user_mismatch— should not happen in normal flow; signals tampering. Surface generically and log.oauth_verify_failed— provider rejected the response (e.g., Steam OpenID assertion failed). Generic retry.Order of operations
You can ship the connections UI first (item 3, read-only listing) without items 1 and 2. The link flow itself is item 1 + 2 together — both are needed to complete an associate round-trip.
Related
/auth/google/associate/authorize— same path as before — so any existing frontend code targeting it should still work.User.has_usable_passwordcolumn to enforce "must keep at least one login method").