Skip to content

OAuth2

auth-methods hands back a per-provider authURL/state/codeVerifier; auth-with-oauth2 exchanges the resulting code server-side, fetches user info, and either signs in the linked record (via _externalAuths), links onto a same-email match, or creates a new record. A built-in preset only needs a client id/secret (plus whatever extra fields its own row below calls out) configured on the collection; any other provider name is a hand-configured provider using its own authorize/token/userinfo URLs — or, with nothing but an extra.issuer set, a generic OIDC provider discovered automatically.

Preset name Scopes (default) Extra config Notes
google userinfo.profile userinfo.email —
github read:user user:email — Falls back to /user/emails when the primary email is private.
apple name email teamId, keyId, privateKey See Sign in with Apple — no static client secret.
microsoft openid profile email User.Read tenant (default common) Entra ID (Azure AD) v2.0 endpoint.
discord identify email —
gitlab read_user baseUrl (default https://gitlab.com) Set baseUrl for a self-hosted instance.
facebook email public_profile —
twitter tweet.read users.read offline.access — X’s OAuth2 has no email scope; PKCE is mandatory (already the default).
linkedin openid profile email — “Sign In with LinkedIn using OpenID Connect”, not the legacy REST API.
slack openid profile email — “Sign in with Slack” (OpenID Connect).
twitch user:read:email — Helix API calls also need the app’s Client ID as a header — handled automatically.
spotify user-read-email user-read-private —

Every preset’s userinfo parsing only ever attributes an email to a record when the provider itself vouches it’s verified (an email_verified claim, or GitHub’s own primary && verified email list) — an unverified address on file with a provider is never enough to sign into (and silently verify) an existing record that happens to share it.

With cookie sessions enabled and settings.meta.appURL configured, the server can drive the whole provider round trip itself instead of the client handling authURL/code exchange: GET .../oauth2/{provider}/start?redirect= sends the browser straight to the provider, and GET .../oauth2/{provider}/callback completes the login and lands the session in a cookie before redirecting back to redirect. The URL to register with the provider is always {appURL}/api/collections/{collection}/oauth2/{provider}/callback. start’s redirect must be same-origin with appURL or an exact entry in CORS_ALLOW_ORIGINS; a * origins entry doesn’t count. An untrusted redirect answers 400 "Untrusted redirect target." directly — the one failure that doesn’t redirect, since redirecting to an untrusted target is exactly what that check prevents. Every other failure redirects to the (now-trusted) redirect with ?cb_error=<code>: cookie_sessions_disabled, app_url_not_configured, oauth2_disabled, or provider_not_enabled. On the client this is cb.auth.signIn.social({provider, mode: "redirect"}) (the default in a browser) or mode: "popup", which opens the same URL in a popup and resolves once it closes.