What is single sign-on and how do OAuth 2.0 and OpenID Connect implement it with Microsoft Entra ID?
howto · identity · oauth · openid-connect · sso · entra-id · azure-ad
Single sign-on is a property - one login session trusted by several apps - not a protocol; OAuth 2.0 supplies the access token an API needs, and OpenID Connect adds the ID token that says who signed in. Microsoft Entra ID is the authorization server (identity provider) for both: one sign-in issues an ID token plus an access token, and a second app in the same tenant reuses that session instead of prompting again. Authorization Code with PKCE is the flow for anything with a sign-in UI; a server-to-server call with no user present uses client credentials instead.
SSO is the effect, OAuth and OIDC are the mechanism
Section titled “SSO is the effect, OAuth and OIDC are the mechanism”“Single sign-on” describes what the user experiences - sign in once, and every app that trusts the same identity provider treats them as signed in - not a wire protocol. Nothing about OAuth 2.0 or OpenID Connect (OIDC) is named “SSO” in either spec; SSO falls out of a design where several client applications point at the same authorization server and that server keeps its own session (a cookie on its own domain) between them. The first app’s redirect to sign in creates that session; every later app’s redirect to the same authorization server finds the session already there and returns straight away, with no login form shown a second time.
App A and App B below are two app registrations in the same tenant; the diagram is the same Authorization Code with PKCE exchange (below) run once for each, with Entra ID’s session cookie the only thing that carries over between them:
sequenceDiagram
autonumber
participant Browser
participant A as App A
participant B as App B
participant IdP as Entra ID
Browser->>A: start sign-in
A-->>Browser: redirect /authorize (code_challenge, openid)
Browser->>IdP: GET /authorize
IdP-->>Browser: login form
Browser->>IdP: credentials
IdP-->>Browser: sets session cookie, redirect + code
Browser->>A: code
A->>IdP: POST /token (code, code_verifier)
IdP-->>A: ID token, access token
Note over Browser,IdP: later, same browser session
Browser->>B: start sign-in
B-->>Browser: redirect /authorize
Browser->>IdP: GET /authorize (session cookie)
IdP-->>Browser: no form, redirect + code
Browser->>B: code
B->>IdP: POST /token (code, code_verifier)
IdP-->>B: ID token (aud = App B), access tokenThe four roles, and where Entra ID sits
Section titled “The four roles, and where Entra ID sits”OAuth 2.0 names four parties in every exchange (Microsoft’s own writeup of the roles is the source for this section):
- Authorization server (also called the identity provider, IdP) - authenticates the end-user and issues the tokens the other parties trust. Microsoft Entra ID is this role for anything registered in a tenant; nothing else in the exchange talks to the user’s password.
- Client - the application requesting access on the user’s behalf: a web app, a single-page app in the browser, a desktop or mobile app, or a web API calling another web API. This is the app developer’s own code, registered as an “app registration” in the tenant.
- Resource owner - the end-user, who can grant or deny the client’s request to act on their data.
- Resource server - the API the client ultimately calls, which trusts tokens issued by the authorization server instead of authenticating the caller itself.
A single app is often client and resource server at once (a web app with its own backing API), and Entra ID is always the authorization server - it never plays client or resource server for someone else’s app.
ID token vs. access token: two tokens from one sign-in
Section titled “ID token vs. access token: two tokens from one sign-in”OAuth 2.0 alone only defines the access token - proof of what the client may do, opaque to the
client, meant to be read only by the resource server it is sent to. It says nothing about who the
user is, because OAuth was designed for delegated authorization (letting an app act on a resource
owner’s behalf), not authentication. OpenID Connect adds the ID token on top: a JWT the
authorization server issues to the client itself, asserting who just signed in
(sub, name, email, iss, aud, exp, …) and signed with the authorization server’s key so
the client can verify it came from Entra ID and was minted for this app (aud matches the client’s
own app ID) and hasn’t expired.
The practical split:
- The client decodes and reads the ID token to know who is signed in and to render the signed-in UI - “log in” in the everyday sense.
- The client never decodes the access token; it attaches it as
Authorization: Bearer <token>and lets whichever resource server issued the requirement for it validate the signature and read the claims to decide what the caller may do.
Requesting the openid scope (alongside whatever API scopes the app needs) is what tells Entra ID’s
/authorize endpoint to include an ID token in the response at all; asking for API scopes without
openid gets an access token with no identity information back, which is an OAuth-only exchange
with no SSO layer on top of it.
Which flow to run
Section titled “Which flow to run”Entra ID exposes the same endpoint shapes as any standards-compliant OAuth 2.0 / OIDC authorization
server - an /authorize endpoint and a /token endpoint under
https://login.microsoftonline.com/<tenant>/oauth2/v2.0/ - and the flow (response_type and
grant_type combination) to drive against them depends on what kind of app is asking:
flowchart TD
Q{User present?} -->|no| CC[Client credentials]
Q -->|yes| K{Kind of app?}
K -->|web app or SPA| PKCE[Auth code + PKCE in the browser]
K -->|desktop or mobile| BR{Broker on the device?}
BR -->|yes| BRK[Broker sign-in, through MSAL]
BR -->|no| SB[Auth code + PKCE in the system browser]- Anything with a user present and a browser available - a server-rendered web app, a
single-page app, and a mobile or desktop app where no broker exists (next item) -
runs Authorization Code with PKCE: the app
redirects the user to
/authorize, Entra ID authenticates them (silently, if an SSO session already exists) and redirects back with a short-lived code, and the app exchanges that code plus the PKCE verifier for an ID token and an access token at/token. PKCE (Proof Key for Code Exchange) is what makes this safe for a “public client” - one that cannot keep aclient_secretconfidential, which includes every SPA and every native app shipped to a user’s device - by tying the code exchange to a random value only the app that started the request knows. - A desktop or mobile app can run that flow itself, but Microsoft now recommends handing
sign-in to an authentication broker rather than opening a browser itself: on Windows
that is WAM (Web Account Manager, part of the OS), on mobile the Microsoft Authenticator or
Company Portal app, each driven through MSAL. The broker still signs the user in through a
browser-based page, so multi-factor authentication and Conditional Access work, and it reuses
the account already signed in to the device - SSO across every brokered app on it, not only
across apps sharing one browser’s cookie - with tokens bound to the device. A plain system
browser with PKCE is the fallback where no broker exists (macOS without Company Portal, Linux
without the broker package, or an authority the broker does not serve, such as Azure AD B2C or
AD FS). The .NET side of this is
dotnet/jwt-authentication-desktop-client. - No user present at all - a daemon, a scheduled job, a service calling another service on its
own identity - runs the client credentials flow instead: the app authenticates directly to
/tokenwith its own client ID and secret (or certificate) and gets back an access token scoped to the app itself, with no ID token, because there is no end-user to authenticate. - The implicit flow (tokens returned directly in the
/authorizeredirect, no code exchange) and Resource Owner Password Credentials (the app collects the password itself) are both legacy: implicit exposes tokens in a browser history and referrer headers with no way to add PKCE, and ROPC hands the password to the client and cannot participate in SSO or multi-factor authentication at all. Entra ID still accepts the implicit flow for compatibility and refuses ROPC outright for any account enrolled in MFA; neither is the answer for a new integration.
Which concrete flow to wire up for a specific client (a JavaScript SPA vs. a .NET desktop app vs. a confidential web app) and how to validate the resulting access token on the resource server are their own, separate questions once these roles and tokens are clear.
The trap: treating the access token as proof of identity
Section titled “The trap: treating the access token as proof of identity”Reading name or email claims off an access token - because they happen to be there for some
resources - and using that to decide who is signed in skips the one guarantee OIDC exists to give:
that the ID token’s aud matches this app, so a token minted for a different client can’t be
replayed into it. The shortcut is tempting because many access tokens Entra ID issues do decode as
JWTs at jwt.ms, and a developer debugging a sign-in sees name sitting there. But
Microsoft’s documentation says a client must treat every access token as an opaque string: its
contents belong to the API it was minted for, and decoding one is a debugging aid, never something
code depends on. That holds even for Microsoft’s own APIs: a Microsoft Graph token is in a
proprietary format that the documented validation rules do not cover, a token for a Microsoft
service may not parse as a JWT at all, and for personal Microsoft accounts it may be encrypted. So
code that reads claims off a Graph token can break with nothing changed on its side - the next
user has a consumer account, or Microsoft changes the format - and an access token minted for a
different API than the one reading it carries no promise that its claims mean what the code
assumes. The client gets what it needs to know about its access token (expiry, granted scopes)
from the token response that came with it, and who signed in from the ID token. Use the ID token
for identity, always.
How to verify it worked
Section titled “How to verify it worked”Sign in to one app registered in a tenant, then, in the same browser session, start the sign-in
redirect for a second app registered in the same tenant: the second app’s redirect to
/authorize should return immediately with no login prompt, because Entra ID’s own session cookie
(ESTSAUTH / ESTSAUTHPERSISTENT) is already there - that round trip with no form shown is what SSO
looks like from the outside. Decoding the returned ID token (at jwt.ms, Microsoft’s
own token inspector, or any JWT decoder) and confirming aud matches the second app’s own client ID,
not the first app’s, confirms the token was minted for that specific client and not merely copied
from the first sign-in.
When this does not apply
Section titled “When this does not apply”A single app with no other app sharing its sign-in has no SSO to speak of - OAuth 2.0 (for the access token an API needs) or plain OIDC (for the ID token alone, with no API to call at all) apply without any of the “same session across apps” behavior being relevant. And an API-to-API call with no interactive user - the client credentials case above - never produces an ID token or any SSO session, because there is no login to share in the first place.
Source: https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols
History: 4 changes
- Body changed
- Body changed
- Made public
- Created