Skip to content

Provider linking: Steam associate flow + connect/disconnect settings UI #39

@amrtgaber

Description

@amrtgaber

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").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions