Social Login and Federation

This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against.

OAuth 2.1 is still an Internet-Draft (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

"Sign in with Google", "Sign in with Microsoft", "Sign in with Apple" and their peers are, mechanically, nothing more than the authorization-code flow described in Authorization Code and PKCE plus the OIDC layer described in OpenID Connect — with your application acting as the OAuth client and the social provider acting as the authorization server. Everything that makes social login distinctive lives above that protocol layer: how you reconcile an external identity against a local account, and the handful of ways real providers deviate from a textbook OIDC implementation.

Social login as an OIDC client flow

The client-side mechanics are identical to any other OIDC login: redirect to the provider’s authorization endpoint with scope=openid …​, receive a code, exchange it for an ID token and access token, validate the ID token, and (optionally) call the provider’s UserInfo-equivalent endpoint. What differs from running your own authorization server is that you do not control the issuer, cannot influence its acr/amr behaviour, and must treat every claim it sends as that provider’s opinion about the user, not a globally authoritative one. The grant selection, PKCE requirement and wire format are unchanged from Flows Overview and Authorization Code and PKCE — this page assumes both.

Account linking and the verified-email trap

The moment a social login succeeds, the client has to answer a question OAuth itself has no opinion on: which local account does this external identity belong to? Two situations arise:

  • First-login provisioning — no local account matches this provider’s sub yet. The straightforward answer is to create one, populated from whatever claims the provider released (name, email, picture).

  • Account linking — the user already has a local account (created directly, or via a different provider) and is now logging in with a new provider for the first time. Matching on sub cannot work here, because sub is provider-scoped and this is a different provider; the only claim usually available to match on is email.

Matching on email is where the trap is:

Never link an external identity to an existing local account on the strength of an unverified email claim. The concrete failure mode: most identity providers let anyone register an account with any email address before that address is verified. An attacker who knows the victim’s email registers an account at Provider B using it, then presents that Provider B identity to your application. If your linking logic matches purely on the string value of email — without checking that Provider B’s email_verified claim is true, or, more strongly, that you have your own independent proof that the victim controls that address — the attacker’s Provider B account gets silently linked to the victim’s existing local account, and the attacker now has an authenticated way in. This is a real account-takeover path, not a theoretical one, and it has been used against production social-login integrations that skipped the verification check.

The safe pattern:

  • Only auto-link on email when the incoming token’s email_verified is true and the local account’s email was itself established through a verified channel (verified at signup, or itself linked from a provider that reported email_verified: true).

  • When email_verified is false, or absent (see GitHub, below), never auto-link — fall back to creating a new, unlinked account, or require the user to prove ownership of the existing account first (log in to it directly, then explicitly "connect" the new provider from an authenticated settings page).

  • Store the link as (provider, provider_sub) → local_account_id, never (email) → local_account_id, so that sub — not a mutable, provider-controlled email string — is the durable key for every subsequent login.

This is exactly the user_identity table shape already documented in Authorization Server & Social Login's "Local account model and endpoints" section, and the findOrCreate logic shown there implements the verified-email-first rule stated here — it is not repeated on this page.

Provider quirks

Real providers deviate from the OIDC spec’s happy path in ways that break naive implementations if un-anticipated.

Apple: form_post response mode and one-shot name release

Sign in with Apple returns its response via response_mode=form_post — the authorization server sends a POST with the code (and, for the hybrid variants, the id_token) to the client’s redirect URI as an HTML form submission, rather than appending them to the URL as a query string or fragment. Apple requires this for every request that also asks for name or email scope, because the response can carry a name that only a POST body, not a URL of bounded length, can be relied on to hold cleanly and because it keeps identity data out of URLs, browser history, Referer headers and server access logs.

POST /callback/apple HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded

code=c1234567890abcdef&state=af0ifjsldkj&id_token=eyJhbGciOiJSUzI1NiJ9...&user=%7B%22name%22%3A%7B%22firstName%22%3A%22Jane%22%2C%22lastName%22%3A%22Doe%22%7D%2C%22email%22%3A%22jane.doe%40privaterelay.appleid.com%22%7D

A server that only implements GET-based redirect handling for every other provider will silently drop this callback — the Apple redirect endpoint must accept POST. The user field above — containing name and email — is released exactly once, on the very first authorization for that user and that client; every subsequent login returns only sub in the ID token, with no name and no email in the response body. The client must persist name/email at that first login, because there is no API to retrieve them again later — losing that row means losing the user’s name and email for that account permanently (Apple also supports private relay email addresses, which forward to the user’s real address but are themselves a stable identifier for that (user, client) pair).

GitHub: OAuth, not OpenID Connect

GitHub’s "Login with GitHub" is a plain OAuth 2.0 authorization-code flow — it does not implement OpenID Connect: there is no id_token, no scope=openid, no discovery document, and no standard claims set. Identity has to be reconstructed by calling GitHub’s REST API with the access token:

GET /user HTTP/1.1
Host: api.github.com
Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a
Accept: application/vnd.github+json

GET /user returns a profile (id, login, name, avatar_url) whose email field can be null even when the user has a public email set, depending on their privacy settings, and carries no email_verified-equivalent flag at all. To get a verified, primary email address, the client must call the emails endpoint separately, with the user:email scope granted, and read the verified flag GitHub does expose there:

GET /user/emails HTTP/1.1
Host: api.github.com
Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a
Accept: application/vnd.github+json
[
  {"email": "jane.doe@example.com", "primary": true, "verified": true, "visibility": "private"},
  {"email": "old-address@example.com", "primary": false, "verified": false, "visibility": null}
]

The account-linking rule above still applies: link only on an entry from this list with "verified": true, never on the bare email field from GET /user. Because there is no ID token, none of the ID-token validation described in OpenID Connect applies to GitHub logins — treat GitHub as an OAuth 2.0 identity source with a proprietary user-info shape, not as an OIDC provider.

Microsoft Entra ID: tenant and issuer handling

Microsoft Entra ID (formerly Azure AD) is a fully OIDC-compliant provider, but its issuer is tenant-scoped, and which base authorization endpoint you use changes what kind of account can sign in:

Authority Who can sign in

https://login.microsoftonline.com/{tenant-id}/v2.0

Only accounts in that specific tenant (a specific organisation’s directory)

https://login.microsoftonline.com/organizations/v2.0

Any work/school account, in any Entra ID tenant

https://login.microsoftonline.com/common/v2.0

Both work/school accounts and personal Microsoft accounts

https://login.microsoftonline.com/consumers/v2.0

Only personal Microsoft accounts

The common/organizations multi-tenant authorities issue an ID token whose iss still names the actual signed-in tenant (https://login.microsoftonline.com/{tid}/v2.0), not the generic authority the client authenticated against — a multi-tenant client must therefore validate iss against the set of tenants it is willing to accept (or against a wildcard-tenant issuer template it explicitly opts into), and read the token’s tid claim to know which tenant actually issued it, rather than assuming a single fixed iss the way a single-tenant integration safely can. Guest ("B2B") users additionally carry the home tenant in their token in addition to the resource tenant they signed into, which matters if the application needs to distinguish a guest from a native member of the tenant. None of the oauth2Login wiring itself changes for Entra — that configuration is documented in full in Authorization Server & Social Login's per-provider section — this is specifically about what to check in the resulting token.

Facebook Login: OAuth with its own token-inspection API

Like GitHub, Facebook Login is a plain OAuth 2.0 flow with no ID token and no discovery document — identity comes from calling the Graph API with the access token:

GET /me?fields=id,name,email HTTP/1.1
Host: graph.facebook.com
Authorization: Bearer EAAG...accesstoken

Facebook’s access tokens are short-lived by default (typically about an hour) unless exchanged for a long-lived token via a separate GET /oauth/access_token?grant_type=fb_exchange_token call, and server-side calls are expected to include an appsecret_proof (an HMAC-SHA256 of the access token keyed by the app secret) as an additional parameter to prove the call originates from the app’s own backend rather than a token thief replaying it from elsewhere. As with GitHub, email on the /me response has no standardised verification flag riding alongside it — Facebook does not release an unverified email at all (it either returns a verified address or omits the field), which simplifies the account-linking decision but should still be confirmed against current platform documentation before being relied on, since platform policies here have changed more than once.

The multi-provider account model

Once a local account can be reached through more than one provider, the account itself has to model that explicitly rather than assuming "one login method per user":

  • One app_user row per person, holding whatever profile data the application itself owns (display name, preferences, application-specific data) — never provider-specific fields.

  • One user_identity row per linked provider, each pointing back at exactly one app_user, keyed by (provider, provider_sub) as established above. A single app_user can have zero (password-only), one, or several user_identity rows.

  • A login UI that, when a user arrives via a provider that maps to zero existing user_identity rows and whose email cannot be safely auto-linked (unverified, or no verified match), should say so plainly — offer to create a new account, or prompt the user to sign in through their existing method first and link the new provider from an authenticated "connected accounts" settings page, rather than guessing.

  • A path to unlink a provider, which must be refused (or must force setting a password / linking another provider first) if it would leave the account with zero remaining ways to sign in.

This is the same app_user / user_identity schema already shown in Authorization Server & Social Login — the model above is provider-agnostic and applies equally whether the authorization server sits in front of your own Spring application or brokers a wider set of upstream identity providers, as described next.

SAML 2.0 vs. OpenID Connect for enterprise SSO

Enterprise customers frequently ask for SAML instead of, or alongside, OIDC. SAML 2.0 is out of scope as a protocol in its own right on this OAuth-focused site — this is the one comparison section needed to make an informed choice between the two for a given integration:

SAML 2.0 OpenID Connect

Wire format

XML assertions, XML-DSig signatures

JSON, JWTs (JWS/JWE)

Browser transport

HTTP Redirect or HTTP POST binding, carrying a base64-encoded, deflated XML payload

Ordinary HTTP redirects and POST/JSON, per this whole reference

Primary token

The SAML assertion, embedded in the response the browser POSTs to the client’s Assertion Consumer Service URL

The ID token, a compact JWT returned from the token endpoint (or, in the legacy hybrid flow, the front channel)

API/mobile fit

Poor — designed for browser-mediated enterprise SSO, awkward for native apps and machine-to-machine calls

Native fit — the same OAuth token machinery that already serves APIs, native apps and SPAs

IdP-initiated flow

A first-class, common pattern: the IdP’s own portal links directly to a service, which accepts an unsolicited assertion

Possible but far less common and standardised; OIDC flows are normally client- (RP-) initiated

Typical home

Long-established enterprise IdPs (Active Directory Federation Services, many campus/government identity federations) predating widespread OIDC adoption

Cloud-native IdPs, social providers, and any new enterprise IdP deployment today

Neither is objectively "better" in the abstract: a service that already federates with a large population of SAML-only enterprise IdPs generally has to keep speaking SAML to them, while any new integration — and essentially every social/consumer login — has no reason to pick SAML over OIDC today. xref: this section from any framework-specific SAML question; the protocol itself is not documented further here.

The broker/hub pattern vs. direct federation

Once more than one or two upstream identity providers are involved, two integration shapes compete:

  • Direct federation — your application itself registers as an OAuth/OIDC client with every provider (Google, Microsoft, GitHub, and any enterprise SAML IdPs), and speaks each provider’s protocol variant directly, exactly as this page describes. Simple for one or two providers; every additional provider adds its own quirks (like the ones above) directly into the application.

  • Broker/hub pattern — your own authorization server (see Authorization Server & Social Login) sits between your applications and the outside world. It is the only thing that federates directly with Google, Microsoft, GitHub, and any SAML IdPs, absorbing every provider-specific quirk in one place; every internal application then speaks one uniform OIDC dialect — your own — regardless of how many upstream providers exist behind it. Keycloak and most commercial IdPs (Auth0, Okta, Entra ID’s own "external identities" feature) are built to run exactly this broker role.

The broker pattern is the better choice as soon as more than a couple of internal applications need the same set of external providers: it means "add a new social provider" or "Apple changed its token shape" is a one-place change in the broker, not an N-way change across every application. It is also what makes SAML enterprise SSO tractable for an OIDC-native application portfolio — the broker absorbs SAML entirely, and every internal service still only ever has to implement OIDC.

flowchart LR subgraph Apps["Your applications"] A1["App A"] A2["App B"] A3["App C"] end Broker["Your authorization server\n(identity broker)"] G["Google (OIDC)"] M["Microsoft Entra ID (OIDC)"] GH["GitHub (OAuth, non-OIDC)"] S["Enterprise IdP (SAML 2.0)"] A1 -->|"OIDC (your issuer)"| Broker A2 -->|"OIDC (your issuer)"| Broker A3 -->|"OIDC (your issuer)"| Broker Broker -->|"OIDC"| G Broker -->|"OIDC"| M Broker -->|"OAuth"| GH Broker -->|"SAML"| S

Spring implementation

Every piece of Spring wiring for social login — oauth2Login(), the OidcUserService / DefaultOAuth2UserService customisation that performs the account-linking lookup described above, and the concrete per-provider YAML for Google, Microsoft Entra ID and Apple (including Apple’s private_key_jwt client assertion) — is documented in Authorization Server & Social Login's "Social login (Google, Microsoft, Apple)" section and is deliberately not repeated here; this page covers the protocol concepts and provider quirks that section’s code implements.