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 ( 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
subyet. 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
subcannot work here, becausesubis provider-scoped and this is a different provider; the only claim usually available to match on isemail.
Matching on email is where the trap is:
|
Never link an external identity to an existing local account on the strength of an unverified |
The safe pattern:
-
Only auto-link on email when the incoming token’s
email_verifiedistrueand the local account’s email was itself established through a verified channel (verified at signup, or itself linked from a provider that reportedemail_verified: true). -
When
email_verifiedisfalse, 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 thatsub— 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 |
|---|---|
Only accounts in that specific tenant (a specific organisation’s directory) |
|
Any work/school account, in any Entra ID tenant |
|
Both work/school accounts and personal Microsoft accounts |
|
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_userrow per person, holding whatever profile data the application itself owns (display name, preferences, application-specific data) — never provider-specific fields. -
One
user_identityrow per linked provider, each pointing back at exactly oneapp_user, keyed by(provider, provider_sub)as established above. A singleapp_usercan have zero (password-only), one, or severaluser_identityrows. -
A login UI that, when a user arrives via a provider that maps to zero existing
user_identityrows 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 |
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.
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.