A small bot needs to read one calendar. Getting it permission, without handing over anyone’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’s out today” to the team chat. Two hours of work, she reckons.
There is one open question. StandupBot has to read calendars it does not own. How does it get in?
The obvious 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 why four different technologies exist. The problem is not that passwords are old-fashioned. It is that a password is the wrong kind of thing to give a program. It proves who you are and unlocks everything you can do, in one piece, with no expiry and no record of what used it. StandupBot needs to read one calendar. A password gives it the whole account.
What Ada needs is answers to three separate questions, from three separate mechanisms — plus a fourth thing that runs 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 protocol for handing 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 plain English, with no prerequisites. Where you see an Under the hood panel, it holds the real requests and responses from Microsoft’s own documentation. Skip every one of them and the article still works.
A password is one secret. Whoever knows it can do everything its owner can do, for as long as it keeps working, and nothing records which program used it for what. Give it to a program and that program is that user — completely, until someone remembers to change it.
That is the part people miss. This is not about whether Ada trusts StandupBot. It is that there is no way to give it less. There is no version of a password that opens only the calendar, only until Friday, and only for one app.
Ada walks past the answer every day and has never once noticed it.
When a contractor arrives to service the air conditioning, reception does not hand him the building master key. 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 deactivates one badge — not every lock in the building.
Nobody thinks this is clever. It is how buildings work. Software took about twenty years longer to get there.
Every piece of this article is already in that lobby. The four technologies are just the software versions of things a receptionist has been doing since 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. The cost arrives later: the day someone leaves and every service that ever had that password has to be found and rotated, or the day StandupBot is compromised and the damage turns out to be Ada’s whole mailbox, files and admin settings rather than one read-only calendar feed.
Everything below is the industry’s answer to a single question: how do you let an app act for a user, for one narrow purpose, without ever giving it something that can impersonate that user everywhere?
OAuth 2.0 is a way to hand over limited access. A user grants an app a narrow slice of what they themselves can do. That is the whole idea. It is about what an app may do — it was never built to say who someone is. Hold on to that; Act II exists because people forgot it.
It names four parts. They are worth learning 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 settled on, and the one Microsoft recommends for nearly 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 everywhere else, because it closes a real attack that older methods left open. Step through it:
Notice 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 good for one thing. That is the whole point.
If the code and the code_verifier check out, Entra ID replies with something like this. Three tokens arrive 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 is what lets StandupBot get a fresh access token tomorrow
without making Ada approve anything again. It is opaque, it lasts longer, and an admin can
revoke 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 get redirected to a trusted page, you sign in, you come back “logged in.” So people treated “I received a valid token” as proof of identity. 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 being replayed at another app that never checked who it 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?”
Companies wrote their own incompatible answers to that for a decade. Then one standard answer was written down and called OpenID Connect.
Back in the lobby, this distinction 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 what the ID check produces. Nobody would accept a badge as proof of who someone is, because the badge was never trying 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 also issue a sign-in document, not just an access token.sub), which tenant (tid), which app it
was issued to (aud), and when it expires.nonce — a random value the app makes first and checks against the ID
token afterwards, which closes the replay hole.It also standardises discovery: every OpenID Connect provider, Entra ID included, publishes a well-known JSON file saying where its endpoints and signing keys live, 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; it 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 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 rivals — OpenID Connect is OAuth plus a standard sign-in document. If your app only ever 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 somewhere is usually a JSON Web Token (RFC 7519), a compact, URL-safe way to package a set of facts so that anyone holding the issuer’s public key can check they have not been changed, without calling the issuer back. It is a format, not a protocol. OAuth does not require it. OpenID Connect requires it for the ID token. Plenty of systems use JWTs with no OAuth anywhere in sight.
The badge in the lobby is printed, not sealed in an envelope. Anyone standing near the contractor can read what it says — his name, his floor, his expiry time. What they cannot do is alter it, because the building’s stamp across the front would stop matching. A JWT works exactly that way, and the next three points all follow from it.
A JWT is three base64url parts joined by dots: header.payload.signature. Click
each part to see what is inside a real Entra ID-shaped ID token.
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 in a JWT payload. Put it in a database and reference it by ID.
Entra ID signs ID tokens with RS256. It signs with a private key 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 says which published key to use,
because Entra ID rotates them. 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 plainly: access tokens for Microsoft’s own APIs, Microsoft Graph included, have no guaranteed format and should be treated as opaque strings. They may look like JWTs today. That is how it happens to work, not a promise, and it can change. The guarantee only applies to access tokens issued for your own registered API. ID tokens are the exception: always a JWT, by specification, meant to be checked by the app that asked for it. If StandupBot starts decoding the Graph access token to “see what is in it,” it is relying 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 produce. None of the three is a running service. Microsoft Entra ID (formerly 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, a Conditional Access rule that blocks sign-in from an unmanaged device or an unexpected country.
All of that happens inside the single “Entra ID signs Ada in” step in Diagram 3. One box, a great deal of work.
Which flow you use depends entirely on what kind of app you are building, and that choice decides your credential and which of the three tokens you even get:
Put it together and 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 scoped 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 shut off centrally without anyone changing
a password.
This is the request StandupBot’s browser redirect sends, shape taken straight from 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
Note openid sitting right next to Calendars.Read in the scope list: that is Act II riding along inside Act I’s request, on one line, exactly
as designed.
Every protection below exists because someone, somewhere, watched the matching attack work.
| What goes wrong | What happens | What stops it |
|---|---|---|
| The code gets stolen | A malicious app on the same device grabs the redirect and trades 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 delivered to an attacker’s URL dressed up as the app’s callback. | Entra ID only redirects to URIs 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, server-side. |
| A stolen token keeps working | A leaked access token stays valid, unnoticed, for a long time. | Short access-token lifetimes (~60 min) plus a refresh token that can be revoked on its own. |
| A sign-in is replayed | A captured sign-in response is submitted again somewhere else. | The nonce from Act II, checked once and thrown away. |
Every fix above is the same move: stop believing what the client says, and check it instead, against Entra ID’s published keys and against the values your own app generated. That is the whole discipline in one sentence.
Click through each layer once and the whole thing should settle into place. Read 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 rotation, 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 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 ships 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 a single line of StandupBot’s code changing.
That is the relationship. Not four competing technologies — one small job, done properly, by four things that each do exactly one part of it.
And if any of it ever stops making sense, go and stand in a lobby for five minutes. Someone will show a photo ID, take a badge that opens one door until six o’clock, and hand it back on the way out. That is the whole design. The only thing the software adds is that the badge is a string, the stamp is a signature, and the desk never goes home.
Discussion