Skip to content
KB

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 token

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.

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 a client_secret confidential, 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 /token with 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 /authorize redirect, 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.

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.

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.

How do I attach a bearer token to every HttpClient request with a DelegatingHandler? (dotnet, howto)How do I attach a bearer tokento every HttpClient request…What is the right way to authenticate a .NET desktop client against an API that issues JWTs? (dotnet, howto)What is the right way toauthenticate a .NET desktop…How do I add a second JWT bearer authentication scheme to an ASP.NET Core API without it conflicting with the existing one? (identity, howto)How do I add a second JWT bearerauthentication scheme to an…How do I add Microsoft Entra ID sign-in to a web app or single-page app? (identity, howto)How do I add Microsoft Entra IDsign-in to a web app or…What database schema links an Entra ID (or other SSO) identity to an existing local user table? (identity, howto)What database schema links anEntra ID (or other SSO)…How do I validate Microsoft Entra ID access tokens on the server to protect an API? (identity, howto)How do I validate MicrosoftEntra ID access tokens on the…How do I coordinate Microsoft Entra ID sign-in between a single-page app and a separately hosted backend API? (identity, howto)How do I coordinate MicrosoftEntra ID sign-in between a…
Open in the graph

Source: https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols

History: 4 changes
  1. Body changed
  2. Body changed
  3. Made public
  4. Created