Skip to main content

OAuth providers

IdentSphere ships built-in support for five social-OAuth providers: Google, GitHub, Microsoft, Apple, and Meta (Facebook). All implement the OAuth 2.1 authorization-code flow, and each can be enabled or disabled independently.

Configuration

The recommended way to configure providers is the admin dashboard (Integrations): open /admin, paste the client ID + secret (or the per-provider equivalent below), and toggle the provider on. Secrets are encrypted at rest and applied live with no redeploy. Only enabled providers are reachable; an unconfigured provider's endpoint returns 404. To stop offering a provider, just toggle it off (or clear its secret).

Google and GitHub can also be configured with environment variables (the legacy path, kept so existing deployments keep working). Microsoft, Apple, and Meta are dashboard-managed. Dashboard config wins when both are present.

// Env-var fallback — Google + GitHub only. Microsoft, Apple, and Meta are
// configured from the dashboard.
AppConfig {
oauth_google_client_id: Some("123-abc.apps.googleusercontent.com".into()),
oauth_google_client_secret: Some(std::env::var("GOOGLE_SECRET").unwrap()),
oauth_github_client_id: Some("Iv1.deadbeef".into()),
oauth_github_client_secret: Some(std::env::var("GH_SECRET").unwrap()),
..Default::default()
}

Per-provider setup

For every provider, register this exact callback URL in the provider's console: {public_base_url}/v1/auth/oauth/{provider}/callback (the {provider} segment is one of google, github, microsoft, apple, meta).

Google

  • Console: Google Cloud → APIs & Services → Credentials → OAuth client (Web).
  • Redirect URI: {public_base_url}/v1/auth/oauth/google/callback.
  • Dashboard fields: Client ID (…apps.googleusercontent.com) + Client secret (GOCSPX-…).

GitHub

  • Console: GitHub → Settings → Developer settings → OAuth Apps.
  • Authorization callback URL: {public_base_url}/v1/auth/oauth/github/callback.
  • Dashboard fields: Client ID (Iv1.…) + Client secret.

Microsoft (Entra ID)

  • Console: Entra admin center → App registrations → your app → Authentication (Web platform).
  • Redirect URI: {public_base_url}/v1/auth/oauth/microsoft/callback.
  • Dashboard fields: Application (client) ID, Client secret (Certificates & secrets), and an optional Tenant.
  • tenant defaults to common (work/school and personal accounts). Set organizations, consumers, or a specific tenant GUID to restrict it.
  • The email is read from the Microsoft Graph OIDC userinfo endpoint; for work/school accounts it may arrive as the UPN (preferred_username).

Apple (Sign in with Apple)

Apple is the odd one out: there is no static client secret. You supply a signing key and IdentSphere mints the short-lived secret for you.

  • Console: Apple Developer → Certificates, Identifiers & Profiles.
    • Create a Services ID (this is the client_id, e.g. com.yourcompany.web).
    • Create a Sign in with Apple key and download the .p8 file (shown once).
    • Note your Team ID (top-right of the portal) and the key's Key ID.
  • Return URL (on the Services ID): {public_base_url}/v1/auth/oauth/apple/callback.
  • Dashboard fields: Services ID, Team ID, Key ID, and the .p8 private key (paste the whole PEM into the multi-line secret box).
  • Notes: Apple uses response_mode=form_post (IdentSphere handles the POST callback automatically), and it only returns the user's name on the first sign-in. Run the Test action after saving — it mints the client secret from your .p8, so a wrong key or id is caught immediately.

Meta (Facebook)

  • Console: Meta for Developers → your app → Facebook Login → Settings.
  • Valid OAuth Redirect URI: {public_base_url}/v1/auth/oauth/meta/callback.
  • Dashboard fields: App ID (client_id) + App Secret.
  • Request the email permission in your app's Login settings — Facebook only returns an email when the app is granted it and the account has a confirmed address. Without an email, sign-in is rejected with a clear error.

Redirect URI to register

Whatever provider you configure, register this exact callback URL in their OAuth app settings:

{public_base_url}/v1/auth/oauth/{provider}/callback

For public_base_url = https://auth.example.com and provider google:

https://auth.example.com/v1/auth/oauth/google/callback

Flow

[browser] [provider]
│ │
│ GET /v1/auth/oauth/google/start? │
│ redirect_to=/dashboard │
│ ──────────────────────────────────────► │ (307 redirect)
│ │
│ GET https://accounts.google.com/... │
│ ───────────────────────────────────────►│
│ │
│ user consents │
│ │
│ GET /v1/auth/oauth/google/callback? │
│ code=...&state=... │
│ ◄────────────────────────────────────── │
│ │
│ → IdentSphere exchanges code for tokens │
│ → IdentSphere looks up / creates user │
│ → IdentSphere sets session cookies │
│ → 307 redirect to /dashboard │

Identity matching

Provider-returned email is the linking key:

  • Existing users.email match → sign that user in to their existing org.
  • No match → create a fresh org with the user as owner, mark email_verified = true.

Where each provider's email comes from:

  • Google — the ID token's email claim (validated against Google's JWKS).
  • GitHub/user, falling back to the primary verified address in /user/emails.
  • Microsoft — the Graph OIDC userinfo email, falling back to preferred_username (the UPN) for work/school accounts.
  • Apple — the email claim in the id_token returned from the authenticated token exchange.
  • Meta/me?fields=email; sign-in is rejected if the app wasn't granted the email permission.

CSRF / state

A 32-byte random state token is minted at /start and persisted in session_cache under IdentSphere:oauth:state:{token} with a 10-minute TTL. The callback compares; missing or expired state is 400.

Open-redirect defense

The redirect_to query parameter is validated at /start:

  • A host-relative path must start with /.
  • It must NOT start with // (protocol-relative URL hijack).
  • An absolute URL is refused unless it exactly matches an entry in the IDENTSPHERE_OAUTH_RP_REDIRECTS allowlist (see Cross-origin OAuth below).

If validation fails, the start endpoint returns 400. With the allowlist empty (the default), every absolute redirect_to is refused — there is no open-redirect surface.

Cross-origin OAuth (Authorization Code + PKCE)

The flow above lands the browser back on a host-relative path and authenticates via first-party cookies. That works when the app and the IdentSphere server share an origin. A cross-origin web app, or a native / mobile app, can't read those cookies — so IdentSphere also speaks the OAuth 2.1 Authorization Code flow with PKCE for those clients.

Enabling it: IDENTSPHERE_OAUTH_RP_REDIRECTS

Set IDENTSPHERE_OAUTH_RP_REDIRECTS to a comma-separated allowlist of the exact absolute callback URLs your relying-party apps will use:

IDENTSPHERE_OAUTH_RP_REDIRECTS=https://app.example.com/auth/callback,myapp://auth/callback
  • Empty by default, which keeps the legacy behavior: only host-relative redirect_to paths are accepted, and the callback completes with first-party cookies. No absolute redirect is permitted.
  • Matching is exact — a prefix, a substring, or a trailing-slash variant of an allowlisted entry does not match. This is what closes the open-redirect hole while still letting named RP origins drive the cross-origin flow.
  • Custom schemes (e.g. myapp://auth/callback) are valid entries, so native deep-link callbacks work.

Because the relying party exchanges the code from its own origin (a CORS request), that origin must also be listed in CORS_ALLOW_ORIGINS. The two settings pair up: IDENTSPHERE_OAUTH_RP_REDIRECTS authorizes the callback URL, CORS_ALLOW_ORIGINS authorizes the browser to call POST /v1/auth/oauth/exchange.

How the flow runs

  1. The RP calls GET /v1/auth/oauth/{provider}/start with an allowlisted absolute redirect_to. On that path a PKCE code_challenge and code_challenge_method=S256 are requiredstart returns 400 without them (only S256 is accepted; plain is refused).
  2. After the user authorizes with the provider, the callback mints a single-use, PKCE-bound authorization code with a 120-second TTL and redirects the browser to redirect_to?code=…. Tokens never transit the URL — only the opaque code does.
  3. The RP posts the code and its PKCE verifier to POST /v1/auth/oauth/exchange (a CORS-enabled, unauthenticated endpoint — the code + verifier are the proof). The server recomputes SHA-256(verifier), constant-time-compares it to the bound challenge, burns the code, and returns { access_token, refresh_token, expires_in, token_type: "Bearer" } in the JSON body.

Tokens come back in the body — not as cookies — so a bearer / native client with no cookie jar on the IdentSphere origin holds both tokens directly. The single-use code is bound to the PKCE verifier that only the initiating RP knows.

The @identsphere/core client implements this end to end; the universal guide has the full React Native walkthrough.

Provider errors

If the provider redirects back with ?error=access_denied (user clicked "deny"), IdentSphere redirects back to your redirect_to with ?oauth_error=access_denied. Your frontend reads oauth_error from the URL to show the user a message.

Recommendations

Which providers to enable depends on your users. A sensible default:

  • Consumer / general apps: Google + Apple. (Apple is effectively required if you ship an iOS app that also offers other social logins.)
  • B2B / workplace apps: Microsoft (Entra ID) + Google Workspace.
  • Developer tools: GitHub.
  • Broad consumer reach: add Meta (Facebook).

Enable only what your audience uses — every provider you turn off is one fewer "sign in with…" button to maintain and one fewer app registration to keep current. You can change the mix anytime from the dashboard; toggling a provider off hides it immediately, with no redeploy. A company that doesn't use GitHub, for instance, simply leaves it disabled and shows only Google, Microsoft, and Apple.

The OAuth handler is structured to make adding further providers straightforward — open an issue if you need one that isn't listed.

Audit

Every successful callback emits an audit entry, one per provider:

  • auth.oauth.google.linked
  • auth.oauth.github.linked
  • auth.oauth.microsoft.linked
  • auth.oauth.apple.linked
  • auth.oauth.meta.linked

Metadata includes existing_user: true/false. New-account sign-ins also emit iam.user.provisioned.