Skip to content

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

Open
spydon wants to merge 3 commits 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 3 commits 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
{
  "subject_token": "<facebook access token>",
  "subject_token_type": "https://supabase.com/auth/token-type/facebook-access-token"
}

The provider is inferred from subject_token_type, so the request carries no separate provider field.

RFC: Token-exchange grant for provider access tokens

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). This is where the account is created, including on Android.
  • Repeat logins → this token-exchange grant.

How

  • grant_type is the standard RFC 8693 URN. The provider is inferred from subject_token_type: a Facebook access token uses https://supabase.com/auth/token-type/facebook-access-token. New providers opt in by registering their own token type, so no new grant is needed and the request carries no separate provider field.
  • 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 one of this project's configured Facebook client ids, 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 generic Invalid subject_token to avoid account enumeration; the client fallback for that edge case is signInWithOAuth (web).
  • Successful sign-ins write a login audit log entry (with the provider trait) and are metered under a dedicated token_exchange login type, since no OAuth authorization flow is involved.

Tests

internal/api/token_exchange_test.go: sign-in of an existing identity (asserting the audit log entry), no-identity rejected, banned user rejected, unconfirmed user rejected, unconfirmed allowed when AllowUnverifiedEmailSignIns is set, token from another app rejected, token issued for a secondary configured client id accepted, invalid token, non-user token, missing subject_token, missing subject_token_type, unsupported subject_token_type, disabled provider. All pass against Postgres.

Notes / follow-ups

  • Multiple client ids: VerifyAccessToken verifies debug_token's app_id against all configured Facebook client ids, matching the id_token grant, which already accepts any configured client id as an acceptable audience. The /debug_token call itself is authenticated with the primary client id.
  • 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.

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
@spydon spydon force-pushed the feat/facebook-access-token-grant branch from abaae77 to 835be4e Compare July 8, 2026 07:32
Comment thread internal/api/token_exchange.go Outdated
Comment thread internal/api/token_exchange.go
Comment thread internal/api/token_exchange.go Outdated
…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.

The provider is inferred from the subject_token_type rather than sent separately: a Facebook access token uses https://supabase.com/auth/token-type/facebook-access-token, so a new provider opts in by registering its own token type. Sign-ins are recorded in the audit log and metered under a dedicated token_exchange login type.
@spydon spydon force-pushed the feat/facebook-access-token-grant branch from 835be4e to d97b05b Compare July 14, 2026 07:50
spydon added 2 commits July 14, 2026 10:03
…d client ids

Match the id_token grant, which accepts any configured Facebook client id
as an acceptable audience. The token-exchange verifier previously checked
debug_token's app_id against only the primary client id (ext.ClientID[0]),
which rejected valid tokens issued for a secondary configured client id.
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.

2 participants