Conditional Access, Part Four: Tokens, Sessions, and the Control You Do Not Have
All right class.
Three posts in. We designed the estate, we fixed the credential, and we dealt with the device. Every one of those controls does its work at the same instant: the moment a token is issued.
Today we deal with what happens afterwards, which is where your attacker actually lives.
Here is the shape of the problem. A user is phished through an adversary-in-the-middle proxy. The proxy sits between the user and the real Microsoft sign-in page, passes everything through faithfully, and watches. The user types their password. The proxy relays it. Entra asks for the second factor. The user approves the push notification, because it is a legitimate prompt for a sign-in they actually initiated. Entra issues a session cookie. The proxy keeps a copy.
Now look at what your Conditional Access estate did. It required multifactor authentication and got it. It required a compliant device and the user was on one. It checked the location and the user was in the office. Every control passed, correctly, because every control was evaluated against a sign-in that was genuinely the user's. Then the attacker takes the cookie to their own infrastructure, replays it, and inherits the session. Your policies never see them, because the attacker never signs in.
That is the gap this post is about. It is not a configuration mistake and you cannot close it by tightening the policies from parts one to three. It is a property of how token-based authentication works, and there are exactly three things in Conditional Access that address it. One of them reached general availability on a platform it did not previously cover this month, which is the main reason this post exists now rather than last year (it's also because I am lazy)
What actually gets issued
You cannot reason about session controls without knowing what is in play, so here is the short version.
When a user authenticates, they do not receive one thing. They receive a set of credentials with different lifetimes and different jobs.
The access token is the bearer credential presented to a resource. It is short-lived, historically around an hour, and it is not revocable in any meaningful sense. Once issued, it works until it expires. Nothing you do in the portal reaches back and kills an access token that is already in the wild.
The refresh token is the longer-lived credential used to obtain new access tokens without the user reauthenticating. This is the one that keeps a user signed in for weeks. It is revocable.
The primary refresh token, on a registered Windows device, is a special refresh token issued to the device rather than to an application, and it is cryptographically bound to that device. Hold on to that word, bound, because it becomes the answer later.
The session cookie in a browser is what carries the sign-in across tabs and page loads. All tabs in a browser session share one session token, which is why browser persistence is an all-or-nothing setting rather than a per-application one.
The default behaviour, absent any policy, is more generous than most people assume. Office clients sit on a rolling ninety-day window, refreshing quietly, prompting only after a password reset or ninety days of inactivity. A browser with no session lifetime settings has no persistent cookie at all and reauthenticates when the browser closes, which is why the "Stay signed in?" prompt exists and why so many users tick it.
Conditional Access is a doorman, not a bouncer
Every grant control in this series so far runs at token issuance. Multifactor authentication, authentication strength, compliant device, named location, sign-in risk. The policy engine looks at the request, decides, and issues or refuses.
Then it stops looking.
That is the property that matters. A doorman checks credentials at the entrance and lets people through. A bouncer walks the room. Conditional Access, in its default configuration, is a doorman. The controls you spent three posts building are all checks at the entrance, and a stolen token is somebody already inside.
Three features change that, and they are not equivalent. Sign-in frequency changes how often the doorman checks again. Continuous access evaluation gives the resource a phone line back to the doorman. Token protection makes the pass unusable by anyone else. Only the third one actually addresses the attack described at the top of this post, and it is the one with the narrowest coverage. Everything below follows from that ordering.
Session controls, and what they do not do
Sign-in frequency sets how long a user can stay signed in before being prompted again for a resource. You choose periodic reauthentication with a value in hours or days, or you choose Every time.
Almost everybody overestimates what this buys them. Three things to be clear about.
It does not revoke anything. Sign-in frequency is evaluated when the session is next evaluated, which is when a new token is requested. An access token already issued lives out its lifetime regardless. Setting sign-in frequency to one hour does not mean an attacker's stolen session dies within the hour. It means the next token request within that session requires reauthentication, and if the attacker holds the session cookie they may well be able to satisfy that.
Every time does not mean every time. Microsoft accounts for five minutes of clock skew, so a user is never prompted more than once in five minutes, and if they completed multifactor authentication in the last five minutes and hit another policy requiring reauthentication, they are not prompted again. On top of that, Every time triggers full reauthentication when the session is evaluated, so a user who closes and reopens their browser inside the session lifetime may not be prompted at all. The control works best against resources that have logic to decide when a client needs a new token, because those resources redirect the user back to Entra only when the session expires.
And it interacts badly with authentication strength, which we covered in part two but which belongs here as well. A resource requiring a passkey and a one-hour sign-in frequency can be satisfied by a passkey presented yesterday and a Windows Hello device unlock today. The two requirements are satisfied independently and at different moments. If you designed a control believing that a short frequency forces the hardware credential to be presented repeatedly, it does not.
Microsoft's own guidance is to limit the number of applications that enforce Every time, on the grounds that reauthentication fatigue pushes users towards approving prompts they did not initiate. That is the correct instinct. Reserve Every time for a small set: privileged role activation, security information registration, and Azure management. Use periodic reauthentication measured in hours everywhere else.
Persistent browser session controls whether the session survives the browser closing. Set it to Never persistent for unmanaged devices and you remove the shared-computer problem where a user walks away from a session that outlives them.
Two mechanics. The control requires the policy to target all resources, because browser persistence is carried by a single session token shared by every tab. And where both are configured, the Conditional Access setting overrides the "Stay signed in?" option in company branding for that user. The pairing that earns its place is the classic one: unmanaged device, sign-in frequency of one hour, persistent browser set to never. That combination is genuinely useful and it is about walk-away risk rather than token theft. Do not confuse the two.
Continuous access evaluation
Continuous access evaluation is the phone line. It establishes a channel between Entra as the token issuer and the resource as the relying party, so that the resource can be told to stop honouring a token, and so the resource can report back when something about the session changes.
It is built on the Open ID Continuous Access Evaluation Profile, so it is an industry mechanism rather than a Microsoft invention, and it is enabled by default whenever both the client and the resource support it. The initial implementation focuses on Exchange Online, SharePoint Online and Teams.
Two categories of event flow down that channel.
Critical events are things like the user being deleted or disabled, the password being changed or reset, an administrator explicitly revoking tokens, or high user risk being detected. The intent is near real time. In practice Microsoft documents a latency of up to fifteen minutes because of event propagation. Fifteen minutes is not instant, and it is enormously better than an hour.
Policy evaluation covers network location changes, and here the enforcement is immediate rather than delayed.
There is a consequence people find counter-intuitive. Access tokens for continuous access evaluation capable resources have extended lifetimes, up to twenty-eight hours rather than the standard sixty to ninety minutes. That sounds like a security regression until you see the trade. A long token is safe precisely because the resource can be told to stop trusting it at any moment through the back channel, and a longer lifetime means fewer refresh round trips. You are trading a scheduled expiry for an event-driven revocation, which is a better deal in every case except one: the case where the client or resource does not support the channel, and then you have a twenty-eight hour token with no back channel. Which is why the coverage query below matters.
Standard against strict location enforcement
Location enforcement under continuous access evaluation has two modes and the difference is worth understanding properly, because it is the one place where the feature can break a working estate.
The problem it solves is that Entra and the resource can see different IP addresses for the same session. Split tunnel networking, proxies, and cloud egress all produce this. Your user authenticates through a corporate egress address that Entra recognises, then their Outlook traffic leaves through a completely different address.
Standard mode is the default and it is suitable for any topology. If Entra sees an allowed address, it issues a short-lived one-hour token that suspends IP checks at the resource until the token expires. Access is preserved. Continuous access evaluation still enforces every other event and policy. This is a deliberate exception that protects productivity, and it means that a session that started from an allowed location is not location-checked at the resource for the next hour.
Strict location enforcement, still in public preview, removes that exception. If the address the resource sees is not permitted by Conditional Access, access is blocked. This is the highest security setting available for location enforcement and it demands something specific of you: your egress addresses must be dedicated and enumerable for both Entra traffic and all resource provider traffic. If your organisation routes Exchange traffic through a different egress than sign-in traffic, and you have not enumerated both, strict mode will block legitimate users and you will find out during business hours.
The guidance writes itself. If you know your network paths well enough to list every egress address with confidence, strict mode is worth enabling and you should test it on a small population first. If you cannot produce that list in ten minutes, you are not ready, and the honest answer is to go and produce the list rather than to enable the setting hopefully.
Token protection
This is the one that actually addresses the attack from the top of this post, and its status changed recently enough that guidance written even a few months ago is now out of date.
Token protection is a session control that requires the sign-in session token to be device bound. When a user registers a supported device, a primary refresh token is issued and cryptographically bound to that device. With token protection enforced, Entra checks that the token presented is one of those bound tokens. A token lifted off the machine and replayed from attacker infrastructure is not bound to that infrastructure, so it does not work.
That is a real answer to token replay rather than a mitigation of it, and it is the only control in Conditional Access that can make that claim.
Here is where it stands as of Microsoft's documentation this month.
| Platform | Native applications | Browser-based applications |
|---|---|---|
| Windows | Generally available | Preview, for supported web apps reaching Azure Resource Manager |
| iOS and iPadOS | Generally available | Not supported |
| macOS | Generally available | Preview, for supported web apps reaching Azure Resource Manager |
Apple platform support reaching general availability for native applications is the material change. A great many published deployment plans still describe token protection as Windows-only preview, and that has not been accurate for some time.
For native applications, enforcement covers Exchange Online, SharePoint Online and Microsoft Teams. On Windows it also covers Azure Virtual Desktop and Windows 365. The browser preview covers Azure Resource Manager, which you configure in Conditional Access as the Windows Azure Service Management API resource.
The device requirements are specific and they are where your deployment will actually stall.
On Windows, you need Windows 10 or newer, Entra joined, hybrid joined or registered. Windows Server 2019 or newer is supported but hybrid joined only, and before you enforce on servers you should test whatever PowerShell modules and scheduled automation run there, because those are exactly the workloads that fail quietly.
On macOS you need version 14 or later with the Microsoft Enterprise single sign-on plug-in, or Platform SSO, and only devices under mobile device management are supported. On iOS and iPadOS you need version 16 or later with the Enterprise single sign-on plug-in, again management-only.
Two practical notes that will save you a support queue. Apple's native Mail and Calendar applications do not support token protection, so users relying on them are blocked once the policy is enforced. Tell them beforehand. And Office perpetual clients are not supported on Windows, which catches organisations still running a licensed-once install somewhere in the estate.
One more detail for anyone writing detections. The string that appears in the sign-in logs for this control is SignInTokenProtection. It changed from Binding back in June 2023, so any query you copied from an older blog post is looking for the wrong value.
The honest hierarchy
Here is my ordering of what actually reduces the impact of token theft, and it is deliberately not the order these features get discussed in most write-ups.
Phishing-resistant authentication is first, because it removes the most common route into the attack. An adversary-in-the-middle proxy cannot relay a passkey. The credential is bound to the origin, so the proxy's lookalike domain cannot complete the handshake and the chain breaks before a token exists. This is why part two came before part four.
Token protection is second, because it addresses the residual case. If an attacker gets a token anyway, through an infostealer on the endpoint rather than through a proxy, binding makes the token useless anywhere else. Its weakness is coverage, not concept.
Continuous access evaluation is third. It does not prevent theft. It shortens the window between you detecting a problem and the session dying, from up to an hour down to about fifteen minutes for critical events, and to instant for location changes. That is response, not prevention, and it is worth having.
Device compliance is fourth, from part three, because it raises the bar on which machines can obtain a token at all.
Sign-in frequency is last, and I would put it a long way last. It is a productivity and walk-away control that people mistake for an anti-theft control.
Now the part I would rather say plainly than leave implied. None of this stops an attacker who owns the endpoint. If malware is running on the machine, inside the user's session, with the user's context, then every control in this series is downstream of a compromise you have already lost. Token protection stops the token being used somewhere else, which is genuinely valuable and narrows the attacker to that one machine. It does not stop them acting from that machine. That is an endpoint detection problem, and the honest architectural statement is that Conditional Access limits blast radius here rather than preventing the event.
Say that in the risk register. It is a better position than claiming a control you do not have.
Why report-only cannot help you here
Report-only mode has been my recommendation in every post in this series. Here it needs a caveat, because it answers a different question than people think.
Report-only tells you whether a policy would break your users. For token protection that is exactly what you need, and Microsoft's own deployment guidance leans on it: run the policy in report-only, capture both interactive and non-interactive sign-ins, analyse for long enough to cover normal application use, then move known-good users into enforcement. The readiness query below is built for precisely that.
What report-only cannot tell you is whether the control is stopping attacks, and the reason is structural rather than a limitation of the feature.
Report-only evaluates sign-ins. A session running on a stolen token that never returns to Entra does not produce a sign-in. There is no event for report-only to evaluate, so the session is invisible to it. And when the attacker's session does refresh, the resulting event looks like the legitimate user, because from Entra's perspective it is: the same account, satisfying multifactor authentication by a claim already in the token, from whatever address the attacker is using.
So report-only answers "will this hurt" and never answers "is this working". For the second question you need the sign-in logs and the queries below, which is the whole reason this series keeps ending in Sentinel.
Mapping onto the personas
| Policy | Persona | Control | Notes |
|---|---|---|---|
| CA102 | Admins | Sign-in frequency four hours, persistent browser never | Shortens the walk-away window on privileged sessions |
| CA105 | Admins | Token protection on Exchange, SharePoint and Teams | Small population, managed hardware, enforce first |
| CA106 | Admins | Sign-in frequency Every time on Azure management | One of the few places Every time earns its cost |
| CA202 | Internals | Sign-in frequency one hour, persistent browser never, on unmanaged devices | Filter for devices from part three does the scoping |
| CA204 | Internals | Token protection, after a full report-only cycle | Coverage gaps live here, not in the Admins block |
| CA007 | Global | Continuous access evaluation left at standard enforcement | Change to strict only when the egress list is complete |
CA105, CA106 and CA204 are new numbers slotted into the persona ranges from part one. That is the numbering scheme doing its job again.
The rollout, in order
Continuous access evaluation needs nothing from you. It is on by default where the client and resource support it. Your job is to measure coverage rather than to enable it, so run the coverage query below and find out which of your traffic is getting a back channel and which is sitting on a long token without one.
Do not enable strict location enforcement until you have an enumerated list of every egress address used by both authentication and resource traffic. Then test it on a small group. Then expand.
Set sign-in frequency and browser persistence for the unmanaged device case first, because it is low risk and it addresses a real walk-away problem. Use the device filter from part three to scope it rather than applying it to everybody.
Then token protection, and take this one slowly.
Start by checking client versions across the fleet, because out-of-date clients are blocked rather than downgraded. Identify the populations you already know will fail: Apple native Mail and Calendar users, anyone on Office perpetual, servers running scheduled automation. Deal with those before the policy exists, not after.
Deploy in report-only for the Admins persona, capturing both interactive and non-interactive sign-ins. Run the readiness query and look at the split between bound and unbound sessions. Give it long enough to cover a normal working cycle, which for most organisations means a month rather than a fortnight.
Move the users who show clean into an enforcement policy. Leave the rest in report-only and work the list down. Then repeat the whole cycle for Internals, expecting the unbound proportion to be considerably higher, because that is where the unmanaged Macs and the odd client versions live.
Run the readiness query on a schedule after you enforce, not just before. The bound proportion drifts as client versions change, and a rollout that was clean in March can quietly develop gaps by August.
The traps
Access tokens are not revocable. Revoking sessions invalidates refresh tokens. Any access token already issued works until it expires. Without continuous access evaluation on that resource, that can be up to an hour. With it, revocation propagates through the back channel instead, which is the point.
Sign-in frequency does not kill sessions. It changes when reauthentication is next required. If your incident response runbook says "set sign-in frequency to one hour to contain the account", the runbook is wrong. Revoke the sessions, disable the account, and rely on continuous access evaluation for propagation.
Every time is not every time. Five minutes of clock skew, and no prompt if multifactor authentication was completed in the last five minutes.
Twenty-eight hour tokens without a back channel. Where a client or resource combination is not continuous access evaluation capable, you can end up with an extended token and no revocation path. Measure this rather than assuming it.
Strict location enforcement needs an enumerated egress list. Enabling it hopefully is how you find out that Exchange traffic leaves through a different address than sign-in traffic, during business hours, from your service desk.
Token protection coverage is narrower than the marketing. Three resources for native applications, plus two more on Windows. Browser support is preview and limited to Azure Resource Manager. Apple support is management-only and requires the Enterprise single sign-on plug-in. Everything outside that list is unprotected, so write down what the control covers before you claim it in an audit response.
Older detection queries look for the wrong string. Binding became SignInTokenProtection in June 2023.
Persistent browser session requires targeting all resources. You cannot scope it to a single application, because the persistence state belongs to the session token that every tab shares.
None of this covers a compromised endpoint. Said in the hierarchy above and repeated here, because it is the sentence most likely to be omitted from a security posture document that cites these features.
Watching it from Sentinel
First, the session replay indicator. One session identifier appearing from more than one country is the cleanest signal available for a hijacked session.
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == 0
| where isnotempty(SessionId)
| extend Country = tostring(LocationDetails.countryOrRegion)
| summarize Addresses = make_set(IPAddress, 20), Countries = make_set(Country, 10), Agents = make_set(UserAgent, 10), Apps = make_set(AppDisplayName, 15), SignIns = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SessionId, UserPrincipalName
| extend AddressCount = array_length(Addresses), CountryCount = array_length(Countries), AgentCount = array_length(Agents)
| where CountryCount > 1 or AgentCount > 2
| extend SessionDuration = LastSeen - FirstSeen
| project UserPrincipalName, SessionId, CountryCount, Countries, AddressCount, Addresses, AgentCount, Agents, Apps, SignIns, SessionDuration
| order by CountryCount desc, AddressCount desc
SessionId is populated for interactive sign-ins and is empty on some flows, so this hunts rather than catching everything. Tune the user agent threshold to your estate before you schedule it, because a user on a laptop, a phone and a tablet legitimately produces several agent strings.
Second, the token replay tell. A sign-in where the multifactor requirement was satisfied by a claim already in the token, arriving from an address that account has never used.
let KnownAddresses = SigninLogs
| where TimeGenerated between (ago(30d) .. ago(1d))
| where ResultType == 0
| summarize by UserPrincipalName, IPAddress;
SigninLogs
| where TimeGenerated > ago(1d)
| where ResultType == 0
| where AuthenticationRequirement == "multiFactorAuthentication"
| mv-expand Detail = todynamic(AuthenticationDetails)
| extend StepDetail = tostring(Detail.authenticationStepResultDetail)
| where StepDetail has "satisfied by claim"
| join kind=leftanti (KnownAddresses) on UserPrincipalName, IPAddress
| extend Country = tostring(LocationDetails.countryOrRegion)
| extend TrustType = tostring(DeviceDetail.trustType)
| project TimeGenerated, UserPrincipalName, IPAddress, Country, TrustType, UserAgent, AppDisplayName, ResourceDisplayName, SessionId, StepDetail
| order by TimeGenerated desc
This is noisy on its own, because travelling users produce it legitimately. It becomes high fidelity when you add a second condition: an empty trustType, an unusual country, or a user agent that has never been seen for that account. Layer those in rather than shipping it raw.
Third, continuous access evaluation coverage. Which of your traffic has a back channel and which does not.
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| mv-expand Processing = todynamic(AuthenticationProcessingDetails)
| extend DetailKey = tostring(Processing.key), DetailValue = tostring(Processing.value)
| where DetailKey == "Is CAE Token"
| summarize CAETokens = countif(DetailValue == "true"), NonCAETokens = countif(DetailValue == "false"), Users = dcount(UserPrincipalName) by ResourceDisplayName, AppDisplayName
| where CAETokens + NonCAETokens > 100
| extend CAEShare = round(100.0 * CAETokens / (CAETokens + NonCAETokens), 2)
| project ResourceDisplayName, AppDisplayName, CAEShare, CAETokens, NonCAETokens, Users
| order by CAEShare asc
Look at Exchange Online, SharePoint Online and Teams first, because those are where the feature is implemented. A low share against those three means a client population that is not capable, and that is a client upgrade project rather than a policy change. Sorting ascending puts the worst-covered application at the top, which is the one holding up your containment time.
Fourth, and this is the one I would build a scheduled rule from, post-revocation activity. When you revoked that account during an incident, did anything keep working?
let Revocations = AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName has_any ("Revoke", "Reset user password", "Reset password", "Disable account", "Update user")
| extend Modified = tostring(TargetResources[0].modifiedProperties)
| where OperationName has "Revoke" or Modified has_any ("StsRefreshTokensValidFrom", "AccountEnabled")
| extend TargetUser = tolower(tostring(TargetResources[0].userPrincipalName))
| where isnotempty(TargetUser)
| summarize RevokedAt = max(TimeGenerated) by TargetUser, OperationName;
let PostActivity = (union SigninLogs, AADNonInteractiveUserSignInLogs)
| where TimeGenerated > ago(30d)
| where ResultType == 0
| extend TargetUser = tolower(UserPrincipalName)
| project TargetUser, SignInTime = TimeGenerated, IPAddress, AppDisplayName, ResourceDisplayName, SessionId;
Revocations
| join kind=inner (PostActivity) on TargetUser
| where SignInTime between (RevokedAt .. (RevokedAt + 4h))
| extend MinutesAfter = datetime_diff("minute", SignInTime, RevokedAt)
| summarize Events = count(), MaxMinutesAfter = max(MinutesAfter), Addresses = make_set(IPAddress, 10), Resources = make_set(ResourceDisplayName, 10) by TargetUser, OperationName, RevokedAt
| order by MaxMinutesAfter desc
Read the results with the token model in mind. Activity in the first few minutes after a revocation is expected, because an access token already issued keeps working until it expires. Activity sixty minutes later, and certainly activity from an address that was not part of the legitimate session, means either the resource had no back channel or the revocation did not cover what you assumed it did. Either way it is a finding about your incident response, not just about the account, and it is the sort of thing nobody discovers until they go looking.
Class dismissed.