0% read
    ← All stories
    Identity & Access · The Lobby Test

    The Bot That Asked
    for a Password

    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.

    24 August 2026 · 18 min read · Nilesh Mohite
    SCROLL
    Microsoft Entra ID OAuth 2.0 OpenID Connect JWT Identity Azure
    Cold open · 08:58

    A two-hour job

    INT. CONTOSO ENGINEERING — TUESDAY MORNING

    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 questionAnswered byIn one line
    How do we write down “this app may do X”?JWTA file format. Three signed parts.
    How does StandupBot get permission for just the calendar, without a password?OAuth 2.0A protocol for handing over limited access.
    How does anything prove the request really came from Ada?OpenID ConnectA sign-in layer built on top of OAuth 2.0.
    Who runs all this for Contoso?Entra IDMicrosoft’s implementation of both protocols.
    Diagram 1 The relationship map click any box →
    runs as IdP authz server issues + signs built on top of MAY be a JWT (not required) ID token MUST be a JWT Microsoft Entra ID the running service formerly Azure AD OpenID Connect the identity layer — who is this? OAuth 2.0 the protocol — what may it do? JWT the format — how is it written down?
    solid = required · dashed = optional
    How to read this

    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.

    Setup · Why the password answer fails

    One password opens everything

    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.

    INT. CONTOSO LOBBY — THE SAME MORNING

    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 lobbyIn the software
    The contractor’s own house keysAda’s password — the thing you must never hand over
    Reception checking his photo IDSigning in — OpenID Connect
    The visitor badge they print for himAn access tokenOAuth 2.0
    The printed, stamped card itselfJWT — 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 badgeThe refresh token
    Reception, the camera, the printer, the list of who’s expectedEntra ID — the desk that runs all of it
    Diagram 2 One secret vs. one scoped document click a tab →
    doors unlocked6
    expiresnever
    revocable per-appno
    who used it, for whatunknown

    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.

    The one-sentence version

    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?

    Act I · Getting permission

    OAuth 2.0 answers “what can this app do?”

    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:

    WHAT ADA IS AGREEING TO

    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:

    Diagram 3 Authorization code + PKCE, hop by hop step →
    Step 1 of 7
    4 lanes: browser, StandupBot, Entra ID, Microsoft Graph

    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.

    Under the hood — the actual /token response

    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.

    Act II · Proving who signed in

    OpenID Connect answers “who is this?”

    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.

    THE GAP

    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:

    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.

    Under the hood — Entra ID’s discovery document

    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.

    Diagram 4 Three tokens, three jobs pick a token →
    The difference that matters most

    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.

    Act III · What a token looks like

    JWT is a file format, nothing more

    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.

    Diagram 5 Anatomy of a JWT click a segment →
    Header — decoded
    
          
    Payload — decoded claims
    
          
    Signature valid — payload matches what Entra ID signed
    try changing "roles" after the fact

    Three things people get wrong:

    1. It is encoded, not encrypted

    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.

    2. The signature is what makes it trustworthy

    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.

    3. Not every token is a JWT you should open

    The bit most tutorials skip

    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.

    Act IV · Who runs all this

    Entra ID is where the other three become real

    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.

    THE PART THAT SURPRISES PEOPLE

    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:

    Diagram 6 Which flow does my app use? pick your app type →

    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.

    Under the hood — what a real /authorize request looks like

    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.

    Act V · What goes wrong

    The failures, and what stops them

    Every protection below exists because someone, somewhere, watched the matching attack work.

    What goes wrongWhat happensWhat stops it
    The code gets stolenA 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 placeTokens 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 uncheckedAn 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 workingA 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 replayedA captured sign-in response is submitted again somewhere else.The nonce from Act II, checked once and thrown away.
    The pattern

    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.

    Colophon · In one picture

    Four layers, four jobs

    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.

    Microsoft Entra IDthe running 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.

    OpenID Connectthe sign-in layer

    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.

    OAuth 2.0the protocol

    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.”

    JWTthe format

    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

    ← Back to all stories