Business Challenge
Post #38 was about what an application may do once it holds a token. This one is about how the token arrives, and it is the last piece of machinery in this stretch of the phase before we return to configuration.
The common expectation is that "which OAuth flow?" is a design decision with several defensible answers. In 2026 on Entra it is mostly not. Single-page apps, server-rendered web apps, desktop applications and mobile applications all use the authorization code flow with PKCE. Daemons use client credentials because there is no user. A web API calling another API uses on-behalf-of. That is the whole decision, and it takes a minute.
What repays study is one level down. The authorize request carries state, nonce, code_challenge, redirect_uri, response_mode — and every one of them exists because something was attacked without it. Read that way, the request is not a form to fill in. It is a list of the ways this exchange has been broken, and the entries are still there because the attacks still work against anything that omits them.
The protocol reference opens by saying it describes low-level protocol details required only when manually crafting and issuing raw HTTP requests to execute the flow, which we do not recommend, and directs you to use a Microsoft-built and supported authentication library instead.
That is right, and it is not an argument for skipping this. A library implements the defences; it does not make the decisions that surround them — which client type you registered as, whether a secret can exist where you put it, what happens to a session after 24 hours. Those are yours, and they are unreadable without knowing what the library is doing.
Architecture
Four parties, three kinds of token
The cast first, because the names recur and two of them are easy to conflate. Four parties are generally involved: the authorization server (Entra — it issues the security tokens your apps and APIs use for granting, denying, or revoking access), the client (the app asking), the resource owner (usually the user, who owns the data), and the resource server (the API holding it).
And three token types, all formatted as JSON Web Tokens:
- Access token — it contains the permissions the client has been granted by the authorization server. This is the one you present to an API.
- ID token — clients use ID tokens when signing in users and to get basic information about them. It tells your app who signed in. It is not an API credential.
- Refresh token — used to get new access and ID tokens, and worth the explicit warning: your code should treat refresh tokens and their string content as sensitive data because they're intended for use only by authorization server.
Neither of the first two arrives by default. A refresh token is only provided if the offline_access scope was requested, and an id_token is only provided if the openid scope was requested. An app that never asked for openid and wonders why it cannot identify the user has found its answer in the scope list.
The flow, and the minute in the middle
Two endpoints: /oauth2/v2.0/authorize and /oauth2/v2.0/token, with an issuer segment that decides the audience — common for both Microsoft accounts and work or school accounts, organizations for work or school only, consumers for Microsoft accounts only, or a tenant identifier.
The user is sent to /authorize, signs in, consents if needed, and the app receives a code. Then the code is exchanged at /token for an access token. The interesting property is the gap between those two: authorization codes are short lived. Typically, they expire after about 1 minute.
One minute is the entire window in which a stolen code is worth stealing — and PKCE exists to make it worthless even then, by binding the code to the client that requested it.
Which flow, by application type
| Application | Flow | Client type |
|---|---|---|
| Single-page app | Auth code + PKCE. More secure than the implicit flow, which is no longer recommended. | Public |
| Server-rendered web app | OIDC to sign in (a session cookie is set), then auth code to call an API | Confidential |
| Desktop and mobile | Auth code | Public |
| Web API calling another API | On-behalf-of — exchange an incoming access token for another access token to be used in outbound requests | Confidential |
| Daemon, service, script | Client credentials — the app's identity, rather than a user's delegated identity | Confidential |
The column that carries weight is the third. Public and confidential is not a setting; it is a fact about where the code runs. A confidential client can keep a secret because it runs on a server you control. A public client cannot, because its code is on a device or in a browser. And the platform treats that as a fact rather than a preference: public clients, which include native applications and single page apps, must not use secrets or certificates when redeeming an authorization code.
Where a secret is legitimate, there is still a better option: all confidential clients have a choice of using client secrets or certificate credentials, and for best security, we recommend using certificate credentials. Post #37's argument applies with more force — a federated credential beats both.
Each parameter, and the attack it stops
| Parameter | What it defends against |
|---|---|
state |
Cross-site request forgery. A randomly generated unique value is typically used for preventing cross-site request forgery attacks, and the app must compare it on return. |
nonce |
ID token replay. Generated by the app, returned in the resulting id_token as a claim, which the app can then verify to mitigate token replay attacks. |
code_challenge |
A stolen code being redeemed by whoever stole it. PKCE, now recommended for all application types, both public and confidential clients, and required for SPAs. |
redirect_uri |
Delivery of the code to an attacker's address. It must exactly match one of the redirect URIs you registered. |
Two footnotes worth carrying. code_challenge_method should be S256, though the spec allows the use of plain if the client can't support SHA256 and Entra supports both — so a hand-rolled client can silently downgrade to a challenge that is not hashed at all. And state has a usage rule that gets broken constantly: do not put URLs or other sensitive data directly in the state parameter, because it travels through the browser; use a key or identifier that corresponds to data stored in browser storage instead.
Why This Architecture Holds Up
Because the platform now enforces what it used to advise
The most interesting sentence in the reference is not advice at all:
The Microsoft identity platform also prevents the use of client credentials in all flows in the presence of an Origin header, to ensure that secrets aren't used from within the browser.
An Origin header means the request came from a browser. So a client secret shipped in front-end code does not produce a warning, a score in a security review, or a finding in a report. The request fails. The platform has taken a rule that was documented for years, widely ignored, and made it a property of the service.
The same shape appears around redirect URIs. A URI typed spa can't be used with non-SPA flows, for example, native applications or client credential flows, and one that is not typed spa fails CORS when a browser tries to redeem a code against it — producing the console error the docs quote verbatim, blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present. The redirect URI is no longer just an address; it is a declaration of what kind of client you are, and the platform holds you to it.
For anyone reviewing an integration, that is genuinely useful: several of the classic findings are now impossible rather than discouraged. It also means an error that looks like a CORS misconfiguration is usually a registration mismatch, which is a different team and a different fix.
Because the implicit flow is gone by displacement, not by removal
The implicit flow — tokens returned directly from /authorize, no code, no exchange — is no longer recommended. But it has not been switched off, and the transition mechanism is worth understanding: the spa redirect type is backward-compatible with the implicit flow. Apps currently using the implicit flow to get tokens can move to the spa redirect URI type without issues and continue using the implicit flow.
So an application can be modernised at the registration and still be running the old flow in its code, indefinitely, with nothing failing. The registration type and the flow in use are independent facts, and only one of them is visible in the portal.
Which makes this an audit question rather than a migration one: for each SPA, what does the code actually request? A response_type of token or id_token at /authorize is the implicit flow regardless of how the redirect URI is typed. The docs' own recommendation stands — we recommend that you use the auth code flow with PKCE for SPAs.
Because the 24-hour SPA session is a design constraint, not a detail
This one reaches into product decisions, and it is easy to miss because it sits in a note near the end of a long reference page.
For refresh tokens sent to a redirect URI registered as spa, the refresh token expires after 24 hours. Additional refresh tokens acquired using the initial refresh token carries over that expiration time, so apps must be prepared to re-run the authorization code flow using an interactive authentication to get a new refresh token every 24 hours.
The access token is separate and shorter — for SPAs, the access token is valid for 1 hour. It is the refresh token's hard 24-hour ceiling that matters, because it cannot be extended by refreshing.
The cause is browser privacy rather than Entra policy, and it dictates the remedy: the re-authentication must be done in a top level frame, either full page navigation or a pop-up window, in browsers without third-party cookies, such as Safari. So a single-page application cannot quietly renew in a hidden iframe on Safari; it must navigate the top-level window once a day.
Anything that breaks when the page navigates — unsaved form state, a live connection, a long-running client-side job — needs to survive that. It is the kind of requirement that is cheap to design for and expensive to retrofit, and the only place it is written down is a note about cookies.
Because two small rules cause disproportionate debugging
Fragments do not reach servers. The use of fragment as a response mode causes issues for web apps that read the code from the redirect. Browsers don't pass the fragment to the web server. The fix is form_post. This is the classic symptom of a server-side app that works in the browser's address bar and receives nothing server-side.
One resource per token. At the token endpoint, the scopes must all be from a single resource, along with OIDC scopes. You can request consent for several APIs at /authorize, but you redeem one resource's token at a time. An app calling three APIs holds three access tokens, and code that treats "the access token" as a single thing will eventually present the wrong one.
And a warning that saves an afternoon: don't attempt to validate or read tokens for any API you don't own, because tokens for Microsoft services can use a special format that will not validate as a JWT, and may also be encrypted for consumer (Microsoft account) users. A Graph token that will not decode is not a bug in your parser.
Key Architecture Decisions
| Decision | What to do | Why |
|---|---|---|
| Choosing a flow | Auth code + PKCE unless there is no user; then client credentials | It covers SPAs, web apps, desktop and mobile. The variation is the client type, not the flow. |
| Implementing it | Use MSAL or Azure Identity; do not craft the HTTP | Microsoft does not recommend raw requests, and the libraries implement the defences by default. |
| Registering a SPA | Type the redirect URI spa, and audit what the code requests |
The spa type is backward-compatible with the implicit flow, so the registration does not prove the flow. |
| Any browser-based client | Never put a secret in it — and expect failure, not a warning | The platform prevents the use of client credentials in all flows in the presence of an Origin header. |
| Confidential client credentials | Certificate over secret; federation over both | For best security, we recommend using certificate credentials — and #37 removes the credential entirely. |
| Designing a SPA's session | Plan for a top-level navigation once a day | The spa refresh token expires after 24 hours and cannot renew in an iframe where third-party cookies are blocked. |
| Server-side app receiving nothing | Switch response_mode to form_post |
Browsers don't pass the fragment to the web server. |
Using state |
Store a key, not the data | Do not put URLs or other sensitive data directly in the state parameter. |
| Calling several APIs | Hold one token per resource | At the token endpoint the scopes must all be from a single resource. |
| A Graph token that will not decode | Stop trying | Tokens for APIs you do not own can use a special format that will not validate as a JWT. |
Closing Thought
What strikes me about this material, after a phase spent on objects and permissions, is how much of it is sedimentary. The flow was not designed in one piece. state was added because requests were forged. nonce because tokens were replayed. PKCE because codes were intercepted on mobile devices. The implicit flow was the original answer for browsers and is now the thing everyone is being moved off. The 24-hour refresh token is not a security decision at all — it is a consequence of browsers deciding, years later, to block third-party cookies.
So the request you send is a sort of changelog. Each parameter is there because the exchange failed without it, somewhere, in a way that mattered enough to change a standard.
That is the argument for using a library, and also the argument for reading this once. The library will get the parameters right. It will not tell you that your SPA needs a top-level navigation every day, that your registration says one thing while your code does another, or that the secret someone added to the front end is not a risk to be weighed but a request that will simply fail. Those are architecture, and they only look like protocol trivia until the day they are the outage.
#40 stays with tokens but turns to what is inside them and how long they last: access, ID and refresh tokens, their claims, their lifetimes, and what can and cannot be revoked once issued.
Comments