A small bot needs to read one calendar. Giving it permission without handing over anybody's password is the whole reason OAuth 2.0, OpenID Connect, JWT and Entra ID exist. The answer is already downstairs, clipped to a visitor's shirt.
Ada is building StandupBot. It is a small internal tool: at 08:59 every morning it reads the team's calendars and posts "who is on leave today" in the team chat. Two hours of work, she thinks.
One question is still open. StandupBot has to read calendars that do not belong to it. How does it get in?
The easy answer is right there in the API docs: sign in as a user, get a session, read the calendar. So Ada adds a setting called CALENDAR_PASSWORD and asks her manager for a service account password to put in it.
Her manager says no.
That "no" is the reason four different technologies exist. The problem is not that passwords are old-fashioned. A password is simply the wrong thing to give a program. It proves who you are and unlocks everything you can do, all in one piece, with no expiry and no record of who used it. StandupBot needs to read one calendar. A password gives it the whole account.
Ada needs answers to three separate questions, from three separate tools — plus a fourth thing that runs all of them for her company.
| The question | Answered by | In one line |
|---|---|---|
| How do we write down “this app may do X”? | JWT | A file format. Three signed parts. |
| How does StandupBot get permission for just the calendar, without a password? | OAuth 2.0 | A standard way to hand over limited access. |
| How does anything prove the request really came from Ada? | OpenID Connect | A sign-in layer built on top of OAuth 2.0. |
| Who runs all this for Contoso? | Entra ID | Microsoft’s implementation of both protocols. |
The main text is in plain English and needs no background. Where you see an Under the hood panel, it shows the real requests and responses from Microsoft's own documentation. You can skip all of them and the article still makes sense.
A password is one secret. Whoever knows it can do everything its owner can do, for as long as it works, and nothing records which program used it for what. Give it to a program and that program becomes that user, completely, until someone remembers to change the password.
This is the part people miss. The question is not whether Ada trusts StandupBot. The problem is that there is no way to give it less. There is no kind of password that opens only the calendar, only till Friday, and only for one app.
Ada walks past the answer every day and has never noticed it.
When a technician comes to service the air conditioning, reception does not give him the master key of the building. They check his photo ID, print a badge and clip it to his shirt. The badge says Floor 3, plant room only, expires 18:00. At 18:01 it opens nothing. If he loses it on the train home, someone switches off one badge, not every lock in the building.
Nobody thinks this is clever. This is simply how buildings work. Software took about twenty years longer to get there.
Every part of this article is already in that lobby. The four technologies are just software versions of what a receptionist has been doing long before any of them existed:
| In the lobby | In the software |
|---|---|
| The contractor’s own house keys | Ada’s password — the thing you must never hand over |
| Reception checking his photo ID | Signing in — OpenID Connect |
| The visitor badge they print for him | An access token — OAuth 2.0 |
| The printed, stamped card itself | JWT — the format the badge is written in |
| “Floor 3, plant room only” | Scopes — Calendars.Read |
| “Expires 18:00” | The exp claim |
| Coming back tomorrow for a fresh badge | The refresh token |
| Reception, the camera, the printer, the list of who’s expected | Entra ID — the desk that runs all of it |
The password route is not slower. It is the fast route, which is exactly why it is tempting at 08:58 on a Tuesday. You pay for it later: on the day someone leaves and every service that ever had that password must be found and changed, or on the day StandupBot is hacked and the damage turns out to be Ada's entire mailbox, files and admin settings, instead of one read-only calendar.
Everything below is the industry's answer to one question: how do you let an app act for a user, for one small purpose, without ever giving it something that can pretend to be that user everywhere?
OAuth 2.0 is a way to hand over limited access. A user allows an app to do a small part of what they themselves can do. That is the whole idea. It is about what an app may do. It was never designed to say who someone is. Remember this; Act II exists because people forgot it.
It has four roles. Learn them once, because every diagram from here on uses the same four:
Entra ID shows Ada a screen: StandupBot wants to read your calendars. Allow? She clicks yes.
StandupBot gets an access token: a short-lived string that says "this app may read calendars for this user, until 09:58". It says nothing about who Ada is, and it cannot be used for anything else.
That is the badge. Floor 3, plant room only, expires 18:00 — printed as a string instead of a card.
The method everyone has settled on, and the one Microsoft recommends for almost every kind of app, is the authorization code flow with PKCE (Proof Key for Code Exchange). PKCE is required for single-page apps and recommended for all others, because it closes a real attack that older methods left open. Step through it:
See what StandupBot never touches: Ada's password. It sends her browser to Entra ID, Entra ID handles the sign-in and the approval on its own site, and StandupBot gets back a short-lived token that is good for one thing only. That is the whole point.
If the code and the code_verifier match, Entra ID replies with something like this. Three tokens come back together, and the next two acts are both about the third one:
{
"token_type": "Bearer",
"expires_in": 3599,
"scope": "Calendars.Read",
"access_token": "eyJ0eXAiOiJKV1Qi...", // for Graph: treat as opaque
"refresh_token": "AwABAAAAvPM1Ka...", // opaque, longer-lived
"id_token": "eyJ0eXAiOiJKV1Qi..." // this one — Act II
}
The refresh_token lets StandupBot get a fresh access token tomorrow without asking Ada to approve again. You cannot read what is inside it, it lasts longer, and an admin can cancel it centrally without touching anything else.
Before OpenID Connect existed, teams fell into a trap. OAuth 2.0 looks like a sign-in system: you are sent to a trusted page, you sign in, and you come back "logged in". So people treated "I got a valid token" as proof of who the user was. It is not. An access token says what an app may do, not who is using it, and nothing stops a token issued for one app from being reused at another app that never checked who the token was meant for.
Two apps ask Entra ID for a token. Both get one. Neither has a standard way to ask "which person is this, and was this token issued to me?"
For ten years, companies wrote their own answers to that, and none of them worked with each other. Then one standard answer was written down and named OpenID Connect.
In the lobby, this difference is obvious. Reception checks a photo ID and then prints a badge. Two separate steps, in that order. The badge is not the ID check; it is the result of the ID check. Nobody would accept a badge as proof of who someone is, because the badge was never meant to prove that.
OpenID Connect is a thin sign-in layer on top of OAuth 2.0. It does not replace OAuth or compete with it. It uses the same flow, the same /authorize and /token endpoints and the same PKCE from Act I, and adds three things:
scope=openid — tells Entra ID to issue a sign-in document too, not only an access token.sub), which tenant (tid), which app it was issued to (aud), and when it expires.nonce — a random value the app creates first and then checks against the ID token, so an old sign-in response cannot be replayed.It also standardises discovery. Every OpenID Connect provider, Entra ID included, publishes a well-known JSON file that says where its endpoints and signing keys are, so an app never has to hard-code them.
Every tenant publishes one of these at a fixed address. Libraries like MSAL fetch it once and cache it. That is how they find the signing keys used in Act III without you ever hard-coding a key:
GET https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
{
"issuer": "https://login.microsoftonline.com/{tenant}/v2.0",
"authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",
"token_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
"jwks_uri": "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
}
That last line, jwks_uri, is the set of public keys that the signature check in Act III depends on.
OAuth 2.0 answers what an app can do. OpenID Connect answers who signed in. They are not competitors; OpenID Connect is OAuth plus a standard sign-in document. If your app only checks scopes, it is doing OAuth. The moment it checks who signed in, it is doing OpenID Connect.
All of the above has to be written down somewhere. That place is usually a JSON Web Token (RFC 7519), a short, URL-safe way to package a set of facts, so that anyone with the issuer's public key can check nothing was changed, without calling the issuer. It is a format, not a protocol. OAuth does not require it. OpenID Connect requires it for the ID token. Many systems use JWTs with no OAuth at all.
The badge in the lobby is printed, not sealed in an envelope. Anyone standing near the technician can read it: his name, his floor, his expiry time. What they cannot do is change it, because the building's stamp on the front would no longer match. A JWT works exactly the same way, and the next three points all come from this.
A JWT is three base64url parts joined by dots: header.payload.signature. Click each part to see what is inside a real ID token in the Entra ID format.
Three things people get wrong:
Anyone can decode a JWT's header and payload with a text editor; sites like jwt.ms do it for you. A JWT is signed, not sealed. It proves nothing was changed after signing; it does not hide anything. Never put a secret inside a JWT. Keep the secret in a database and refer to it by ID.
Entra ID signs ID tokens with RS256. It signs with a private key that nobody else has, and anyone can check the signature with the matching public key published at the jwks_uri from Act II. The header's kid tells you which published key to use, because Entra ID changes its keys from time to time. An app that decodes a token without checking the signature has built a token reader, not a token checker.
Microsoft's own documentation says it clearly: access tokens for Microsoft's own APIs, including Microsoft Graph, have no fixed format, and you should treat them as strings you cannot read. They may look like JWTs today. That is just how it works right now, not a promise, and it can change. The promise applies only to access tokens issued for your own registered API. ID tokens are different: they are always JWTs, by design, meant to be checked by the app that asked for them. If StandupBot starts decoding the Graph access token to "see what is inside", it depends on behaviour Microsoft never promised to keep.
OAuth 2.0 and OpenID Connect are documents. They describe how a server should behave. JWT is a format anyone can create. None of the three is a running service. Microsoft Entra ID (earlier called Azure AD) is the running service: the system that plays the OAuth authorization server role and the OpenID Connect provider role at the same time, for every app registered in Contoso's tenant.
Reception is not a rulebook either. It is a desk, a person, a camera, a badge printer and a list of who is expected today. Entra ID is that desk — and the app registration is the line on the list with StandupBot's name on it.
Registering StandupBot creates an app registration, and everything in Act I depends on it: a client_id for the app, the exact redirect URIs it is allowed to return to (Entra ID refuses any other), a credential if it is a confidential client (a secret, or better, a certificate), and the list of permissions someone has approved.
Entra ID does not just issue tokens and walk away. It also decides whether Ada gets signed in at all: password, MFA prompt, or a Conditional Access rule that blocks sign-in from a personal device or from an unexpected country.
All of that happens inside the single "Entra ID signs Ada in" step in Diagram 3. One box, a lot of work.
Which flow you use depends on what kind of app you are building, and that choice decides your credential and which of the three tokens you even get:
Put together, it is one sentence: a user signs in to Entra ID, which issues a JWT ID token proving who they are (its OpenID Connect role) and a limited access token saying what their app may do (its OAuth 2.0 role). Both are signed with keys published at the discovery document's jwks_uri, both expire, and both can be switched off centrally without anyone changing a password.
This is the request that StandupBot's browser redirect sends, in the exact shape shown in Microsoft's documentation:
GET https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=a1b2c3d4-...
&response_type=code
&redirect_uri=https%3A%2F%2Fstandupbot.contoso.com%2Fcallback
&scope=openid%20profile%20Calendars.Read
&state=4f8a...
&code_challenge=E9Melhoa2...
&code_challenge_method=S256
See openid sitting right next to Calendars.Read in the scope list. That is Act II travelling inside Act I's request, on one line, exactly as designed.
Every protection below exists because someone, somewhere, saw the matching attack succeed.
| What goes wrong | What happens | What stops it |
|---|---|---|
| The code gets stolen | A harmful app on the same device catches the redirect and exchanges the code for tokens itself. | PKCE. The attacker has the code but not the original code_verifier, so the exchange fails. |
| Tokens go to the wrong place | Tokens are sent to an attacker's URL that pretends to be the app's callback. | Entra ID only redirects to URIs that were registered exactly, in advance. |
| A token is trusted unchecked | An app decodes a JWT and believes it without checking the signature, issuer or audience. | Always check the signature, iss, aud and exp — every time, on the server. |
| A stolen token keeps working | A leaked access token keeps working for a long time and nobody notices. | Short access token lifetimes (about 60 minutes), plus a refresh token that can be cancelled on its own. |
| A sign-in is replayed | A captured sign-in response is sent again somewhere else. | The nonce from Act II, checked once and then thrown away. |
Every fix above does the same thing: stop believing what the client says and check it, against Entra ID's published keys and against the values your own app created. That is the whole discipline in one sentence.
Click through each layer once and everything should fall into place. Read from bottom to top: a format, wrapped by a protocol, wrapped by a sign-in layer, run in production by a real service.
The real, running system. Plays both the OAuth authorization server role and the OpenID Connect provider role for Contoso's tenant: app registrations, sign-in, Conditional Access, consent, key changes and issuing tokens.
A thin standard on top of OAuth 2.0. Adds scope=openid, the ID token and nonce. It answers "who signed in", which OAuth alone never claimed to answer.
The limited-access framework: four roles, the authorization code flow, PKCE, scopes, access tokens and refresh tokens. Answers "what may this app do", never "who is this".
A signed, three-part, base64url document (RFC 7519). Not tied to any protocol. OpenID Connect's ID token must be one; OAuth does not require it. Encoded, not encrypted.
StandupBot goes live that afternoon. It never sees Ada's password. Its access token expires before lunch and renews itself. Its ID token proves, to anyone who checks the signature, exactly who signed in and when. And if Ada leaves Contoso next year, someone disables one account in one place, and every token downstream stops working within the hour — without changing a single line of StandupBot's code.
That is how the four fit together. Not four competing technologies, but one small job done properly by four things, each doing exactly one part of it.
And if any of this ever stops making sense, go and stand in an office lobby for five minutes. Someone will show a photo ID, take a badge that opens one door till six o'clock, and return it on the way out. That is the whole design. The only thing software adds is that the badge is a string, the stamp is a signature, and the desk never goes home.
Discussion