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.
tenantdefaults tocommon(work/school and personal accounts). Setorganizations,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
.p8file (shown once). - Note your Team ID (top-right of the portal) and the key's Key ID.
- Create a Services ID (this is the
- Return URL (on the Services ID):
{public_base_url}/v1/auth/oauth/apple/callback. - Dashboard fields: Services ID, Team ID, Key ID, and the
.p8private 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
emailpermission 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.emailmatch → 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
emailclaim (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 topreferred_username(the UPN) for work/school accounts. - Apple — the
emailclaim in the id_token returned from the authenticated token exchange. - Meta —
/me?fields=email; sign-in is rejected if the app wasn't granted theemailpermission.
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_REDIRECTSallowlist (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_topaths 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
- The RP calls
GET /v1/auth/oauth/{provider}/startwith an allowlisted absoluteredirect_to. On that path a PKCEcode_challengeandcode_challenge_method=S256are required —startreturns 400 without them (onlyS256is accepted;plainis refused). - 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. - 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 recomputesSHA-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.linkedauth.oauth.github.linkedauth.oauth.microsoft.linkedauth.oauth.apple.linkedauth.oauth.meta.linked
Metadata includes existing_user: true/false. New-account sign-ins also emit
iam.user.provisioned.