Microsoft Entra ID under the hood

Recently I saw a Microsoft Entra ID architecture poster on LinkedIn, put together by Srawon Kumar Reddy Mula, and it is a good one: six numbered steps across the top, an architecture panel underneath, security and governance columns down the side. User signs in, request goes to Entra ID, authenticate, evaluate access, issue token, access application. A dashed arrow loops back from the last box to the third and carries the label “token renewal / continuous access evaluation”.

(Source: LinkedIn post by Srawon Kumar Reddy Mula)

And I was interested in how that actually works, because the diagram does not really show it. One dashed arrow covers a lot of ground. How long does that token live? What happens to it when you revoke someone? Does the loop do anything at all if the client never asked for it?

So I went and found out. I built the flow against a real tenant, one working sample per box, and measured the parts the poster fits into an icon. Nothing on the poster is wrong, and none of what follows is a correction. It is the answer to a question a picture that size cannot hold.

Two comments under the original post were asking better versions of the same question. Ernie Prescott pointed out that “Hybrid Identity with Active Directory” was sitting quietly in the highlights list. It was doing far more work than it looked like. Sajeed Mullaji wrote that PIM only solves the activation window, not the payload. Both of them named a boundary the picture compresses, so both of them got a sample.

Six samples, one per box

The repository is at github.com/steefjan1/entra-id-end-to-end. Six samples, one per box, every one of them run against a real tenant.

The dashed arrow is doing the most work on the whole poster

Continuous access evaluation gets one dashed line and one label. In practice it is a narrow feature with a wide reputation.

It covers five critical events: the account is deleted or disabled, the password changes, MFA is enabled for the user, an administrator revokes all refresh tokens, or Identity Protection detects high user risk. It reaches Exchange Online, SharePoint Online, Teams and Microsoft Graph, and even that list needs footnotes. The client matrix marks Teams partially supported in every row. SharePoint Online does not support the user risk event. Azure Resource Manager is not on the list at all. And none of it reaches a client that did not ask for it. Asking means declaring the cp1 capability, so that Entra ID treats the session as CAE aware.

Two clients, one line of configuration apart

That last part is a single line of MSAL configuration, and everything hangs off it. So the first sample in the repository measures the difference rather than describing it. It signs the same user in twice against Graph, once with clientCapabilities: ['cp1'] and once without. Then it revokes the sessions and polls with both tokens until each one stops working.

Here is what that returned on my own tenant this morning. Client A, the one declaring cp1, came back with a token good for 1439 minutes. Client B, identical except for that one line, came back with 65 minutes. Both sit exactly where the documentation says they should: 20 to 28 hours for a continuous access evaluation session, and the randomized 60 to 90 minute band for an ordinary one.

Then I revoked the user’s sessions.

Client A stopped working four seconds later. I know it was four seconds rather than roughly, because the claims challenge carries the timestamp: the nbf value Entra ID sent back decodes to 07:57:38Z and Graph turned the token away at 07:57:42.

Client B kept answering 200 for the rest of its 65 minutes. Revoking sessions invalidates refresh tokens and browser cookies. Microsoft’s own guidance on removing a user’s access says the rest: for applications using access tokens, the user loses access when the access token expires. Nothing in the tenant shortens that window.

The challenge is a timestamp, not a capability

The challenge itself is worth a look, because it is not the one most articles show:

{"access_token":{"nbf":{"essential":true,"value":"1787731058"}}}

Not a capability negotiation. A timestamp. The resource is saying: your token predates the revocation instant, bring me one issued after it. essential: true means the client does not get to negotiate.

Which is the whole of that dashed arrow, drawn out. Notice what the client does with the challenge: it clears its cache, asks again with the claims parameter, and gets a fresh token. The user sees none of it. That is the part worth implementing. It is also why declaring cp1 without handling the challenge is worse than not declaring it at all: the client will loop, retrying a token the resource has already told it to replace.

Run that once against your own tenant and the dashed arrow stops looking like a safety net.

“Evaluate access” is not continuous, and that is the part people get wrong

One passage from Microsoft’s own documentation deserves a place on that poster more than anything already on it: policies targeting roles or groups are evaluated only when a token is issued, and if a user already has a valid token before being added to the role or group, the policy does not apply retroactively.

Read that against how offboarding usually works. You remove someone from a group. The group was the thing granting access. You now believe access is gone. It is not, because the token in the client’s cache does not care about your group change, and continuous access evaluation does not cover group membership. Microsoft documents that replication as taking up to one day. There is an optimization that brings it down to two hours. It applies to policy updates rather than to group membership, and the documentation says plainly that it does not cover all scenarios yet. Their own recommended workaround is to revoke the user’s sessions by hand.

The same applies to a new Conditional Access policy and to a role assignment. Which is a strange thing to discover during an incident.

Where authority actually lives

Ernie’s point deserves the space. In a hybrid tenant, the box marked “authenticate” is not where the answer comes from. It comes from wherever the account state currently lives, and the delay between the two directions is not symmetric.

With password hash synchronization, disabling an account in Active Directory does not immediately end cloud access. Microsoft puts the window at up to 30 minutes, and adds that sync never carries password expiry or account lockout state to Entra ID at all. Pass through authentication and federation enforce those states at sign in, immediately. That is a real architectural difference hiding behind a single line item in a highlights list.

The other direction has no delay because it has no mechanism. Neither Connect Sync nor Cloud Sync provisions a user disable back to Active Directory. Disable the cloud account and Kerberos, NTLM and LDAP access on premises continues exactly as before. Conditional Access does not sit in that path natively. Entra Private Access for domain controllers went GA in January 2026. It is the way to put policy in front of Kerberos, and it works by putting sensors on the domain controllers rather than by extending the token flow.

There is a clock on Connect Sync now

There is a clock attached to this now. Microsoft published a phased transition plan from Connect Sync to Cloud Sync. From July 2026 it starts telling tenants their individual transition windows through the Message Center, Connect Health and targeted email. The tenants where Cloud Sync already covers everything go first. Source of authority conversion for individual users went GA in January 2026. The Active Directory to Entra ID boundary is moving under people while they are still drawing it as one arrow.

Sample 05 in the repository does not fix any of this. It measures it. The report lists every principal by where its authority lives, and flags synced objects whose last sync is stale. It also names the case that should worry you most: a principal whose authority is on premises but whose privilege is in the cloud.

It also ships a coverage matrix that contacts nothing and takes a second to read. Six controls down the side, five surfaces across the top, and the column for on-premises Kerberos, NTLM and LDAP reads no from top to bottom. Conditional Access, MFA, continuous access evaluation, sign-in risk, device compliance, sign-in logs. Not one of them reaches that surface natively. That column is the highlights-list bullet, drawn honestly.

PIM controls the window, not the payload

Sajeed’s comment is the best one line summary of privileged access I have read this year, so I built a script around it.

PIM answers when a role is active. It says nothing about what the role can do while it is. An eligible assignment with a one hour activation limit, MFA on activation and an approval step looks like a strong control on a dashboard. If the role behind it grants four hundred actions including application credential management, you have not reduced the blast radius. You have scheduled it.

So sample 04 prints both numbers on the same row. Per assignment it prints the activation maximum duration, and whether the role requires MFA and approval. Next to that sits the number of resource actions the role definition actually grants. Then a payload risk verdict that flags wildcards, and a list of high impact actions.

What the report found on my own tenant

I ran it on my own lab tenant expecting to demonstrate a point. It found something instead. Two service principals, one of them a monitoring integration, both holding Directory Writers. That role carries microsoft.directory/servicePrincipals/appRoleAssignedTo/update, which grants application permissions to any application in the tenant. It also carries microsoft.directory/groups/members/update. Neither assignment expires. Nobody would think to look at a monitoring identity when auditing privilege. No PIM dashboard would show it as a problem either, because PIM is not involved at all.

That tenant has no P2, which turned out to matter in an instructive way. The report falls back to the plain role assignment endpoint and prints the window columns as unavailable. The action counts do not change, because a role definition grants what it grants however the assignment happened. A tenant with no PIM is the more alarming case, not the less: every assignment is standing, permanent, with no window to shorten.

Writing that fallback also caught a bug in my own scoring. Entra ID writes its largest wildcard in words rather than asterisks. Global Administrator’s payload includes microsoft.directory/allEntities/allProperties/allTasks, and matching wildcards on * alone ranked Global Administrator below a billing role on a naive action count. In a script whose entire purpose is measuring the payload, the measurement inverted the ranking at exactly the row that mattered most.

Sajeed made the point about ERP duties, and he is right that Entra ID cannot see inside the application. That is the honest boundary of this work. Sample 04 reports Entra’s half. The duty separation inside your finance system is a separate project with separate tooling. Pretending PIM covers it is how audits get closed without anything getting safer.

Step one says “user signs in”

In a real Azure tenant, most sign-ins are not users. They are service principals, managed identities, and pipelines.

Almost none of the controls in the diagram apply to them the way people assume. Conditional Access for workload identities covers single tenant service principals registered in your tenant. It does not cover managed identities at all. It does not cover multitenant or Microsoft applications. Its conditions cover location, service principal risk and authentication context, and block is the only grant control it offers. If your plan was to require MFA for a pipeline, there is no such thing. Continuous access evaluation for workload identities reaches Microsoft Graph and nothing else.

What you can do is remove the secret. Sample 06 deploys workload identity federation, so a GitHub Actions workflow reaches Azure with no stored credential at all. It also ships an inventory script. That script ranks every workload identity in the tenant by credential type, days to expiry and the application permissions it holds on Graph.

What 348 service principals look like

Run against my lab tenant it found 348 service principals, 246 of them Microsoft first party, leaving 102 worth inspecting. Twenty seven of those are managed identities. That is the number behind the claim at the top of this section. One person, one lab, and a hundred non human principals authenticating, without anybody ever having thought about them as sign-ins.

The report was also wrong twice, and both mistakes are instructive. Graph returns signInAudience as null for a managed identity, and null !== undefined, so my check labelled all twenty seven of them multitenant. And Azure rotates a managed identity’s certificate itself, leaving stale entries on the service principal, so the report announced five certificates that expired two thousand days ago as high risk. Two categories of confident nonsense, both crowding out the one row in that tenant that genuinely mattered: a multitenant application holding Directory.ReadWrite.All and User.ReadWrite.All.

A managed identity has no credential you rotate and no Conditional Access at all. Its entire blast radius is its permissions and its Azure RBAC scope. I made the same argument from the other end when I looked at Logic Apps agent loop security: once the identity is managed for you, the permissions are the only surface left to get wrong. Which is the argument for reading the permissions column rather than the expiry column. I had to be wrong in public on my own tenant to see it.

Conditional Access deserves the same pipeline as your code

The shield in the middle of the poster is a set of policies someone clicked together in a portal. Conditional Access is the tenant wide half of authorization. The per API half I covered separately in securing AI APIs with authentication and authorization in Azure API Management. Neither one substitutes for the other. No history, no review, no test, and no way to answer “what breaks if I enable this” other than enabling it.

Sample 03 treats it as code. Policies are JSON files with placeholders instead of hard coded object IDs. A guard refuses to ship a policy that targets users, carries a grant control and does not exclude the break glass group, because Conditional Access has no “except the person who wrote it” fallback. Everything deploys in report only unless you opt in twice.

The part I would steal even if you ignore the rest is the test runner. Graph exposes the What If evaluation as an API, so every pull request can check a set of hypothetical sign-ins against your live policies. The most valuable assertion in the file is the one that says no policy of your own catches the break glass account. Run it forever.

Where this is the wrong answer

Not all of this is worth doing.

If you run a small tenant with a handful of policies and one administrator, the portal is fine. A Conditional Access pipeline is machinery you will maintain instead of using. Without Entra ID P2, PIM and access reviews are not a decision you get to make, and standing assignments with a tight scope plus an honest quarterly review beat pretending otherwise. If you are already on Cloud Sync and everything works, the 2026 migration is not a project you need to start this quarter.

And the measurement scripts come with a warning I will repeat here. The revocation stopwatch revokes a real person’s sessions. The propagation watcher requires you to disable a real account. Tell the person first.

This is the same conclusion I keep arriving at, and I wrote up how I got here in my Azure security journey. The one thing I would not skip, whatever size you are, is running the two read only reports. The entitlement report in sample 04 and the sync report in sample 05 need no licence beyond what you already have. They change nothing. And they answer questions about your own tenant that the diagram cannot.

The Microsoft Entra ID samples repository

Everything above ships as working code at github.com/steefjan1/entra-id-end-to-end, MIT licensed. Six samples, each with its own README, its own permissions list, and its own section on where it is the wrong answer. Every write supports a dry run. Nothing deletes anything.

I deployed sample 01 and signed into it. Sample 06 compiles clean and I have not deployed it yet, which is exactly the kind of thing a diagram would let me leave out and a README should not.

Five defects that only showed up on deployment

Deploying it for real was worth more than writing it. Five defects turned up that no amount of template validation would have caught. The repository documents every one, with the actual error text:

  • Entra ID validates preAuthorizedApplications against the scopes that already exist on an app, not the ones you create in the same request. Exposing a scope and pre-authorizing a client for it has to be two calls with a wait in between.
  • az writes to stderr for entirely ordinary things. Windows PowerShell under ErrorActionPreference = 'Stop' treats that as fatal, which killed the same script three times for three unrelated reasons.
  • The execution policy on a normally configured Windows machine blocks an unsigned .ps1 azd hook. I stopped using PowerShell for hooks at all.
  • Node 20 reached end of life in April 2026. App Service quietly substitutes the nearest LTS it actually has in your region, rather than failing.
  • And one of my own. I read an azd timeout warning as a deployment failure, and changed a working deployment strategy on the strength of it. The app had already started successfully two minutes after azd stopped watching. That sample’s README writes it up under a heading telling you not to make it.

The number on my screen while writing this

The token in front of me as I write this carries 75 minutes, the exact average Microsoft documents for the randomized 60 to 90 minute window. It is a small thing to see the number rather than read it. It is also the entire argument of this post in one line.

Run any of it against your own tenant. If it does not behave the way I have described, that is worth an issue rather than a comment thread. The whole point of putting code behind an architecture picture is that the picture can then be wrong out loud.

Leave a Reply