Skip to content

feat(token): add RFC 8693 token-exchange grant for provider access tokens#2609

Open
spydon wants to merge 1 commit into
masterfrom
feat/facebook-access-token-grant
Open

feat(token): add RFC 8693 token-exchange grant for provider access tokens#2609
spydon wants to merge 1 commit into
masterfrom
feat/facebook-access-token-grant

Conversation

@spydon

@spydon spydon commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Adds an RFC 8693 token-exchange grant to POST /token so a client can sign in with a provider-issued access token instead of an OIDC id token:

POST /token?grant_type=urn:ietf:params:oauth:grant-type:token-exchange
{
  "provider": "facebook",
  "subject_token": "<facebook access token>",
  "subject_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

Why

Native Facebook login on Android (supabase/supabase-flutter#1287) only mints an OIDC id token (which carries email/email_verified) on the first authorization. Every subsequent login returns a classic access token with no profile claims and no id token, so the id_token grant is unusable for the common repeat-login case. This is a Facebook platform limitation (Limited Login is iOS-only). See facebook/facebook-android-sdk#1132.

The split, matching how Firebase and Auth0 handle Facebook:

  • Signup / first login → OIDC id token via the existing id_token grant (has the email claims).
  • Repeat logins → this token-exchange grant.

How

  • grant_type is the standard RFC 8693 URN; the provider-specific bit is the subject_token_type + provider (matched case-insensitively), so new providers opt in rather than needing a new grant (the design the reviewer asked for; happy to fold provider into subject_token_type if preferred).
  • Verification is provider-specific via a new provider.AccessTokenVerifier interface. Facebook's implementation calls /debug_token (app token sent as a Bearer header, not in the URL; transport errors have the URL stripped so the token can't leak), and requires is_valid, an app_id matching this app, and a USER token type. It returns the token's user_id as the subject.
  • Sign-in-existing only: debug_token returns no profile claims, so the grant looks up the existing auth.identities row (provider, provider_id=user_id) and issues a session. It does not create accounts (a brand-new user signs up via the id-token first-login path), and rejects banned users and unconfirmed users (unless AllowUnverifiedEmailSignIns is set), matching the other sign-in paths. A missing identity returns a clear error; the client fallback for that edge case is signInWithOAuth (web). All sign-in failures return a generic Invalid subject_token to avoid account enumeration.

Tests

internal/api/token_exchange_test.go: sign-in of an existing identity, case-insensitive provider, no-identity rejected, banned user rejected, unconfirmed user rejected, unconfirmed allowed when AllowUnverifiedEmailSignIns is set, token from another app rejected, invalid token, non-user token, missing subject_token / provider, unsupported subject_token_type, unsupported provider, disabled provider. All pass against Postgres.

Notes / follow-ups

  • Single client-id (intentional): VerifyAccessToken verifies against the primary configured Facebook client id only. Multiple client ids per project are out of scope for this grant (single-app is the expected config); the id_token grant's multi-id handling is unaffected.
  • Session-info tokens: Facebook's attenuated, no-user-data token can be added later as another subject_token_type for defense in depth once the client can produce one; the server flow is identical.
  • link_identity support is stacked in feat(token): support link_identity in the token-exchange grant #2614.

Comment thread internal/api/provider/facebook.go Outdated
Comment thread internal/api/token_access_token.go Outdated
Comment thread internal/api/provider/facebook.go
@spydon spydon force-pushed the feat/facebook-access-token-grant branch from 1d3f849 to 16f15c6 Compare July 3, 2026 08:39
Comment thread internal/api/provider/facebook.go
@spydon spydon force-pushed the feat/facebook-access-token-grant branch 3 times, most recently from 9b63794 to f1c4695 Compare July 6, 2026 09:42
@spydon spydon changed the title feat(token): add access_token grant for signing in with a Facebook access token feat(token): add RFC 8693 token-exchange grant for provider access tokens Jul 6, 2026
Comment thread internal/api/token_exchange.go Outdated
@spydon spydon force-pushed the feat/facebook-access-token-grant branch from f1c4695 to 0280c58 Compare July 6, 2026 10:01
Comment thread internal/api/token_exchange.go Outdated
@spydon spydon force-pushed the feat/facebook-access-token-grant branch 2 times, most recently from 05f0a39 to abaae77 Compare July 7, 2026 19:57
Comment thread internal/api/token_exchange.go
…kens

Adds grant_type=urn:ietf:params:oauth:grant-type:token-exchange so a client can sign in with a provider-issued access token (subject_token) instead of an OIDC id token.

Facebook's native Android login only mints an OIDC id token (carrying the email claims) on the first authorization, so signup stays on the id_token grant. On subsequent logins Facebook returns only a classic access token with no profile claims. This grant verifies the token belongs to the app via a provider-specific AccessTokenVerifier (Facebook: /debug_token, checking app id, validity and user-token type), reads the provider subject from it, and signs in the existing identity. It does not create accounts.
@spydon spydon force-pushed the feat/facebook-access-token-grant branch from abaae77 to 835be4e Compare July 8, 2026 07:32
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.

1 participant