Sign In with Microsoft Entra ID in Spring Boot: OIDC, OAuth 2.0, PKCE, Client Credentials, SAML
▶ Watch on YouTube & subscribe to The Stack Underflow
You open a protected page, sign in with Microsoft, and land back, signed in. In between, the browser, your Spring Boot app and Microsoft Entra ID trade redirects, a one-time code and three kinds of token. This page follows every step, then the API that receives the token, and a background job with no user at all.
The one-line version: OAuth 2.0 grants access; OpenID Connect adds sign-in. The ID token is for your app; an access token is for one API.
Last verified against Microsoft Learn (Microsoft identity platform) and the Spring Security 7.1.1 reference: 1 October 2026. Scope: a server-side Spring Boot 4.1 web app (Spring Security 7.1.1), Entra ID v2.0 endpoints, one tenant, plain Spring Security properties (what the Spring Cloud Azure starter sets up for you). Single-page, mobile and multi-tenant apps and External ID are out of scope.
The words, as memory pegs
The video reuses the office building from the Spring Security video. The pegs are analogies to help you remember, not definitions:
| Word | Memory peg (analogy) | What it actually is |
|---|---|---|
| Identity provider (Entra ID) | Head office’s security team | The service that authenticates the user and issues tokens |
| Authorization code | The claim ticket | A short-lived, one-time code returned through the browser |
| PKCE | A secret the desk keeps | A random verifier; only its hash travels in the first redirect |
| ID token | A signed letter of introduction | A JWT saying who signed in, addressed to your app |
| Access token | The key card for one building | A token for one API, its audience |
| Refresh token | The renewal slip | Gets new access tokens without asking the user |
| Redirect URI | The only address for the ticket | Must exactly match one registered on the app |
| State and nonce | Matching stubs | State comes back with the code; nonce is sealed inside the ID token |
Entra ID is Azure AD’s new name since 2023; Microsoft says the login URLs, APIs and MSAL stay the same.
Setup: register with head office, point Spring Boot at it
- Register the app in Entra: you get a client ID and tenant ID, and you add a Web redirect URI. Spring’s default template is
{baseUrl}/login/oauth2/code/{registrationId}, so a registration namedentrauses/login/oauth2/code/entra. - Give it a credential. Microsoft recommends a certificate or federated credential for production. A client secret is limited to 24 months at most (Microsoft recommends under 12), and Microsoft says secrets should not be used in production.
- Permissions. Delegated permissions (scopes) let the app act for a signed-in user and appear in the
scpclaim. App roles appear in therolesclaim, for assigned users or admin-approved apps. - Configure Boot with the registration and the issuer:
spring:
security:
oauth2:
client:
registration:
entra:
client-id: <client-id>
client-secret: <secret>
scope: openid, profile, email, offline_access
provider:
entra:
issuer-uri: https://login.microsoftonline.com/<tenant-id>/v2.0
At startup, Spring reads the discovery document at that issuer: the authorize, token, UserInfo and logout endpoints, and the JWKS URI with Entra’s public signing keys. If the document can’t be fetched, or its issuer doesn’t exactly match the configured one, the app does not start.
The front channel: through the browser
A visitor asks for a protected page with no badge, so Spring Security saves the request and redirects the browser to Entra with:
response_type=code,client_id,redirect_uri,scope- a random
stateandnonce - a PKCE
code_challenge: the hash (S256) of a secret verifier Spring keeps
PKCE is on by default since Spring Security 7: in 7.1.1, ClientRegistration’s client settings default to requireProofKey = true, and the reference says you must set it to false only if the provider doesn’t support PKCE for confidential clients. Microsoft recommends PKCE “for all application types, both public and confidential clients”.
Entra then authenticates the user, with MFA and Conditional Access as needed. To the app that’s a black box; it never sees the password. On first use, Entra asks the user to consent to the requested permissions, unless an admin already consented for everyone. Cancel or decline, and the browser comes back with error=access_denied.
On success, Entra redirects to the redirect URI with the code and the state. The code expires after about a minute and is useless without the PKCE verifier. Everything here travels as browser redirects, so no secret and no token goes this way; only the short-lived code does.
The back channel: redeeming the claim ticket
The callback reaches Spring Security’s login filter, and now the app talks to Entra directly:
- State first. The returned
statemust match the one saved in the session. It proves this browser started the request, which stops cross-site request forgery. A missing or different state matches no saved request, so Spring rejects it (authorization_request_not_found) before any token is requested. - Token request. The app posts the code, the PKCE verifier and its credential to the token endpoint.
- Three tokens back: an ID token, an access token for Microsoft Graph (sign-in requested only OIDC scopes), and, because
offline_accesswas requested, a refresh token.
A used or expired code, or a wrong verifier, gets invalid_grant. A wrong or expired credential gets invalid_client.
The ID token: check the letter, then issue the badge
Spring Security validates the ID token before trusting it:
| Check | What must hold |
|---|---|
| Signature | Verifies with the public key named by the token’s kid, from Entra’s JWKS endpoint. Keys rotate, so never hard-code one |
iss | Equals the configured issuer |
aud | Contains this app’s client ID |
exp | Not expired |
nonce | Matches the nonce saved at the start (stops an old ID token being replayed) |
Any failure is an invalid ID token. Spring then builds an OidcUser from the ID token plus UserInfo from Microsoft Graph, fetched with that access token. Authorities: OIDC_USER and one SCOPE_ authority per scope. Entra’s roles claim needs a GrantedAuthoritiesMapper. The user goes into the SecurityContext (the badge), saved in the HTTP session, and the browser returns to the page it first asked for.
Never call an API with the ID token. Microsoft’s guidance: “You shouldn’t use an ID token to call an API.”
Calling an API: one key card per building
The controller calls the orders API with a RestClient and Spring’s OAuth2 interceptor, which gets a token from the authorized client manager:
- Each access token is for one API, its audience. The Graph token from sign-in won’t open the orders API. With no token for that API yet, Spring sends the browser back to Entra for the API’s scope (for example
api://orders/Orders.Read), the same code flow with PKCE. If the Entra session is alive and consent was given, there is no prompt. - Lifetime. Entra access tokens get a random default lifetime between 60 and 90 minutes (75 on average).
- Refresh. An expired token is renewed with the refresh token, without asking the user; Entra returns a new refresh token that replaces the old one. A revoked or expired refresh token gets
invalid_grant; Spring drops the stored token, so the next call goes back to Entra. - Opaque to the client. The token travels as
Authorization: Bearer .... The client never reads it; only the API validates it.
Sign-out: at the app and at head office
Sign-out has two layers. Spring Security invalidates the HTTP session and clears the SecurityContext. With OidcClientInitiatedLogoutSuccessHandler, the browser then goes to Entra’s end-session endpoint with the ID token as a hint and a registered post-logout redirect URI. Entra ends the user’s session for this app and can notify other apps through their front-channel logout URLs.
With local logout only, the Entra session survives, and the next sign-in may need no password.
The API’s side: checking the key card
The orders API is a Spring Boot resource server; its filter chain is the Spring Security video. What’s specific to Entra:
- Token version. Set the API registration to issue v2 tokens (
requestedAccessTokenVersion: 2); the default (null) gives v1 tokens, with a different issuer, which would fail a v2.0 issuer check. - Audience. In a v2 token,
audis the API’s client ID. Set theaudiencesproperty: accepting another API’s token is the confused deputy problem. - Scopes and roles. Spring maps
scptoSCOPE_authorities by default; Entra’srolesclaim needs its own converter, for example toROLE_authorities. - Answers. An invalid token gets 401 with a
WWW-Authenticateheader; a valid token without the needed scope or role gets 403. - Calling another API as the user? Don’t forward the token; exchange it at Entra with the on-behalf-of flow.
No user: client credentials
A scheduled job signs in as itself: no browser, no consent screen, no ID token, no refresh token. It sends its client ID, its credential and the scope {API ID URI}/.default, which means the app permissions an admin granted for that API. The token carries roles, not scp. Spring’s authorized client manager reuses the cached app token until a minute before it expires. A wrong or expired credential gets invalid_client: an expired secret is a classic outage.
Which protocol when
| Situation | Use | You get |
|---|---|---|
| Sign a user in | OpenID Connect (auth code + PKCE) | ID token (+ access token, + refresh token with offline_access) |
| Call an API for that user | OAuth 2.0 access token (delegated scopes) | One access token per API (scp) |
| A service with no user | Client credentials | App-only access token (roles), no refresh token |
| An API calling another API as the user | On-behalf-of | A new access token for the downstream API |
| An enterprise app that only speaks SAML | SAML 2.0 | A signed SAML assertion, no access token for APIs |
For SAML, Entra posts a signed assertion to the app; Spring Security’s SAML 2.0 login processes it at POST /login/saml2/sso/{registrationId}.
Pause & Prove
1. After sign-in, your app holds an ID token and an access token. It needs to call your orders API. Can it send either?
Neither. The ID token is for your app, never for calling an API. The access token from sign-in is for Microsoft Graph, and each access token is for one API, so it won’t open the orders API. Spring gets a separate access token for the orders API’s scope, and the API checks that aud is its own client ID.
2. A nightly job uses the client credentials flow with Entra ID. What does it get back?
- An ID token and an access token. No user signed in, so there is no ID token.
- An access token and a refresh token. Tempting, but Microsoft says refresh tokens are never granted with this flow; the job simply asks again.
- Only an access token. ✓ An app-only access token, carrying
roles, notscp. - Only a refresh token. There’s nothing to refresh; the job needs an access token to call the API.
Peg drill
Identity provider (head office’s security team) · authorization code (the claim ticket) · PKCE (a secret the desk keeps) · ID token (a signed letter of introduction) · access token (the key card for one building) · refresh token (the renewal slip) · state and nonce (matching stubs).
Before / after this video
- Before: Security and sign-in words: 401, 403, JWT, OAuth, PKCE, the primer.
- Sibling: Why Spring Security returns 401 or 403: the API’s filter chain, step by step.
Sources
Checked on 1 October 2026:
- Microsoft identity platform and OAuth 2.0 authorization code flow: PKCE recommendation, exact redirect URI match, ~1 minute codes,
access_denied,invalid_grant/invalid_client,offline_access, refresh token replacement, single-resource scopes - OpenID Connect on the Microsoft identity platform: OIDC extends OAuth 2.0,
/v2.0authority and discovery, UserInfo on Graph, nonce, two-step sign-out, front-channel logout - ID tokens: proof of authentication, don’t call APIs with it,
audandnoncechecks - Access tokens: 60–90 minute default lifetime, opaque to clients,
aud, confused deputy,requestedAccessTokenVersion, key rotation andkid - Access token claims reference: v2
aud= API client ID,scponly in user tokens,rolesfor client credentials - Client credentials flow: app’s own credentials, no refresh tokens,
/.default - Add and manage app credentials: certificates or federated credentials for production, 24-month secret limit
- Add app roles:
rolesclaim, application permissions need admin consent - On-behalf-of flow: token exchange,
requested_token_use=on_behalf_of, don’t relay tokens - SAML single sign-on protocol: redirect binding in, POST binding out, signed assertion
- New name for Azure AD: rename, unchanged login URLs, APIs and MSAL
- Spring Security: OAuth2 Login core configuration: default redirect URI template,
issuer-uridiscovery - Spring Security: Authorization grant support and ClientRegistration.java at 7.1.1: PKCE default (
requireProofKey = true) - Spring Security: OAuth2 Login advanced:
OIDC_USER,SCOPE_authorities,GrantedAuthoritiesMapper - Spring Security: OIDC logout:
OidcClientInitiatedLogoutSuccessHandler - Spring Security: Resource Server JWT:
issuer-uri,audiences,scptoSCOPE_, custom prefix converter - Spring Security: SAML 2.0 Login overview:
POST /login/saml2/sso/{registrationId} - The video’s accuracy notes trace the Spring internals (state check and
authorization_request_not_found, UserInfo retrieval, startup issuer check, client-credentials token reuse,invalid_granthandling) to the Spring Security 7.1.1 and Spring Boot 4.1.1 source.
Change notes
- 1 Oct 2026: first published.
Not affiliated with or endorsed by Microsoft, VMware Broadcom or the Spring team. Microsoft Entra is a trademark of the Microsoft group of companies; Spring is a trademark of Broadcom Inc. and/or its subsidiaries. Found a mistake? Tell us in the video’s comments and we’ll correct this page.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →Prefer email? Get the free newsletter: one failure, traced step by step, about once a week.