TL;DR
- In the OAuth authorization code flow, the web server's exchange of the authorization code for tokens shows up in Entra as a non-interactive sign-in.
- If the app is a confidential client (it authenticates with a client secret or certificate), that sign-in inherits the user's IP address, OS and device state from their interactive sign-in.
- If the app is a public client, it still inherits the user's OS and device state but uses the web server's own IP address, so location-based Conditional Access policies block it.
- Conditional Access evaluates those inherited values, not just the log entries: the user's device state can carry the vendor's server past a device-trust policy.
- This is why the Claude M365 connector gets blocked when other apps don't. Excluding the vendor's IP ranges is a risky workaround. The real fix is on the vendor's side.
Background
Many organisations implement Conditional Access location controls. A common example is a policy that blocks all authentications from non-trusted IP addresses when coming from workstation operating systems (Windows or macOS). I recently looked into an issue where a specific application (Anthropic's Claude M365 connector), integrated with Entra to grant the app delegated access to Microsoft resources on behalf of the user (e.g. SharePoint, Exchange Online, etc.), was being blocked by exactly this kind of policy.
The authentication was occurring in two steps according to the Entra sign-in logs:
- The user signs in interactively (from their workstation IP address), and this succeeds.
- There is then a non-interactive sign-in for the user that comes from the vendor's IP range and as such is blocked by the Conditional Access policy mentioned above.
The issue is that organisations (for obvious reasons) don't want to make an exemption to this kind of policy (and generally haven't had to for other applications). Since the general sign-in flow appears to be fairly standard, shouldn't this be an issue for other applications too if it's happening for this one?
Quick Refresher: The Authorization Code Flow
The OAuth 2.0 authorization code flow is the standard way a web application gets delegated access to Microsoft resources on behalf of a user. The steps below describe a web application with a server-side back end, which is the model used by the application in this post. (For single-page apps and mobile/desktop apps, step 4 happens on the user's device itself.) At a high level:
- The user browses to the application, which redirects them to Entra (
login.microsoftonline.com/.../authorize). - The user signs in. Entra logs this as an interactive sign-in, and it comes from the user's own device and network.
- Entra redirects the user's browser back to the application with a short-lived authorization code.
- The application's web server sends that code directly to Entra's
/tokenendpoint and receives access and refresh tokens in return. The user isn't involved in this step, so Entra logs it as a non-interactive sign-in. This request comes from the application's web server, not from the user's device.
Step 4 is where things get interesting: if Conditional Access evaluated it purely on where the request came from, it would be the vendor's IP address, not the user's.
(For comparison, the On-Behalf-Of flow is used when a middle-tier API takes a token it has received from a client and exchanges it for a new token to call another API downstream as that same user.)
My Initial Hypothesis
I think that the application in question may be using the OAuth On-Behalf-Of (OBO) flow rather than the authorization code flow.
My hypothesis is that in the authorization code flow, the non-interactive part of the sign-in (the part where the web server exchanges the authorization code for a token) is assessed using the sign-in properties of the interactive part of the sign-in (where the user signs in at login.microsoftonline.com/authorize). So if the user is signing in from their client device at IP address 203.0.113.10 and the web server at IP address 198.51.100.20 requests a token, then both parts of the sign-in are assessed as coming from 203.0.113.10.
The second part of my hypothesis is that this is different for the OBO flow: the second (non-interactive) part of the sign-in is assessed using the web server's IP address.
Before testing this, I went looking to see what Microsoft and the wider community had to say.
What Do Microsoft Docs and Security Researchers Say?
-
Microsoft's documentation on non-interactive sign-ins says:
"The IP address of non-interactive sign-ins performed by confidential clients doesn't match the actual source IP of where the refresh token request is coming from. Instead, it shows the original IP used for the original token issuance."
So this points to the difference between confidential and public clients potentially being the issue, although here it is specifically talking about token refreshes, not necessarily the original authentication. If that's the case, the OBO flow may be a red herring.
-
A Cloudbrothers article on Continuous Access Evaluation says:
"I was able to refresh for a new access token from a completely different IP address and device, but the only change that Entra ID (Azure AD) registered was the change in location."
Could this test have used a public client refresh token, which would explain why the IP address in the sign-in logs changed?
-
GCIT posts about having the issue with the Claude M365 connector, the same application I was looking into.
Their fix was to exclude both Anthropic's IP ranges and the Claude apps from the affected policy. I'm not sure why both exclusions would be needed. I also don't think you should have to do this, as other applications don't need it.
-
An about365 article talks about how Power Automate service accounts have their non-interactive sign-ins coming from Microsoft infrastructure IP addresses rather than the client IP address.
Interestingly, they refer to having this problem on the token refresh only and don't mention experiencing this issue on initial sign-in. Their solution is to exclude the Microsoft Flow Service from location-based Conditional Access policies.
My Revised Hypothesis
Based on the above reading, my new hypothesis is that the difference comes down to confidential vs public client flows, rather than the authorization code flow vs the OBO flow.
Confidential vs public clients: a confidential client is an application that can securely hold a credential (a client secret or certificate), and it uses that credential to prove its identity when it redeems the authorization code. A public client, such as a mobile/desktop app or a single-page app, can't keep a secret, so it redeems the code without one.
For confidential client flows, the non-interactive part of the sign-in performed by the web server inherits the IP address from the interactive part of the sign-in performed on the end user's device.
For public client flows, the non-interactive part of the sign-in performed by the web server does not inherit this information, and the web server's own IP address is used.
So the theory here is that the vendor may have configured their Entra app registration to use a public client flow rather than a confidential client flow (with a client secret/certificate), and that is what is causing the issue.
Who Else Has Posted About This Issue With the Claude M365 Connector?
It turns out this isn't an isolated case:
- GCIT (as listed above)
- Otto IT
- pariswells.com
- Issues raised directly on Anthropic's GitHub:
- A question posted on Microsoft Q&A
The common thread across these posts is the same fix: add exclusions to your Conditional Access policies. What I couldn't find in any of them was why this connector needs an exclusion when other applications don't. So it's time to test the theory.
The Test
Setup
I will set up an app registration. On one device I will set up a basic Python web server, and on another I will perform authentication. These two devices will be on two different networks and as such have two different public IP addresses.
The first device is a Linux device that will host the basic Python web server, which redirects visiting users to login.microsoftonline.com/authorize to sign in to the Entra app registration we created. We'll call this the "web server".
The second device will be used to navigate to the web server's /login URL (redirected to login.microsoftonline.com) and authenticate. We'll call this the "client device". This is a Windows device that is Microsoft Entra hybrid joined, so that we can also see whether device signals are evaluated any differently between confidential and public clients.
| Device | Public IP | OS |
|---|---|---|
| Web server | 61.69.x.x | Linux |
| Client device | 115.130.x.x | Windows (Microsoft Entra hybrid joined) |
In Entra, I've also configured:
- the client device's IP address (
115.130.x.x) as a trusted named location (office-ip), the only trusted IP address in the tenant - a Conditional Access policy, "Windows/MacOS - Block Auth Off Trusted Network", which does what it says it does
- a second policy, "Require Hybrid Joined Device", which blocks authentications to all resources unless they come from an Entra hybrid joined device
We'll do two tests:
- The first where the redirect URI is set to "Web" and therefore requires a client secret for the web server to authenticate (confidential client)
- The second where the redirect URI is set to "Mobile and desktop applications", and therefore the web server is unable to use a client secret (public client)
In both tests the user authenticates from the client device, which is inside the trusted location, while the web server redeems the authorization code from an untrusted IP address.
Results
Note: IDs, IP addresses, locations and unrelated policy names have been partially redacted in the screenshots below.
In both tests, the interactive part of the sign-in is the same: the user authenticates from the client device, and it's recorded at the client device's IP address, matching the office-ip trusted named location.
Here's how the two tests compare:
| Confidential client | Public client | |
|---|---|---|
| Interactive sign-in | Success, from client device IP (trusted office-ip) | Success, from client device IP (trusted office-ip) |
| Non-interactive IP address | Client device's, trusted office-ip | Web server's, no named location |
| Non-interactive device details | Windows, hybrid joined, client device's device ID | Windows, hybrid joined, client device's device ID |
| Browser / user agent | Web server's (Python Requests) | Web server's (Python Requests) |
| Client credential type | Client secret | None |
| Windows/MacOS - Block Auth Off Trusted Network | Not Applied | Failure (block) |
| Require Hybrid Joined Device | Not Applied (device excluded) | Not Applied (device excluded) |
| Overall sign-in result | Success | Blocked by Conditional Access |
Test 1: Confidential Client
With the app registration configured as a confidential client, the sign-in succeeded. The non-interactive sign-in (the web server's exchange of the authorization code for tokens) is recorded with the client device's IP address and the trusted named location, even though the request was made from the web server on a different network:
Because the inherited IP address is a trusted location, the location policy never applies, and the sign-in completes.
Test 2: Public Client
With the app registration configured as a public client, the sign-in failed, blocked by Conditional Access.
The interactive sign-in is unchanged, but the non-interactive sign-in now shows the web server's IP address, and with it no trusted named location:
The device details, on the other hand, are still the client device's (Windows, hybrid joined, and the same device ID), even though the request came from the Linux web server:
And this is where Conditional Access shows its hand. The policy detail for "Windows/MacOS - Block Auth Off Trusted Network" shows both conditions matching: the device platform matched on Windows, inherited from the client device, and the network matched on the IP seen by Entra ID, which is the web server's:
Meanwhile "Require Hybrid Joined Device" wasn't applied at all, because the device condition matched the client device's device ID, and that device's hybrid joined state is excluded by the policy:
So Conditional Access doesn't just record these values in the sign-in logs, it evaluates them: the inherited device state let the non-interactive sign-in past the device policy, while the web server's own IP address is what got it blocked by the location policy. The user signs in successfully in their browser, and the authorization code redemption is then blocked from underneath them, which is exactly the symptom from the start of this post.
Conclusions
From these tests, we can conclude that for confidential client flows, the web server's exchange of the authorization code for a token (logged as a non-interactive sign-in) inherits the IP address of the original interactive sign-in completed by the user, and Conditional Access evaluates it that way.
On the other hand, for public client flows this IP address is not inherited, and the non-interactive sign-in produced by the exchange of the authorization code for a token uses the web server's IP address, not the user's.
In both cases, the device details (operating system, device ID and Entra join state) are inherited from the client device, while the browser and user agent are the web server's own.
| Confidential client | Public client | |
|---|---|---|
| IP address | Inherited from client device | Web server's own |
| Operating system | Inherited from client device | Inherited from client device |
| Device ID / Entra join state | Inherited from client device | Inherited from client device |
| Browser / user agent | Web server's own | Web server's own |
| Client credential type | Client secret | None |
| Location-based CA policy | Evaluated against the client device's IP | Evaluated against the web server's IP |
This goes a step beyond Microsoft's documentation, which only describes the IP inheritance for confidential clients' refresh token requests. The same behaviour applies to the initial authorization code redemption.
Implications
What This Means for the Original Issue
This lines up with the issue I was looking into: the code redemption inherits the Windows or macOS operating system from the user but uses the vendor's IP, resulting in a Conditional Access block by the location-based policy.
It also most likely answers the question from the start of this post. What matters is where the authorization code is redeemed, not where it's obtained: other applications that redeem the code on their own infrastructure rather than on the user's device do so as confidential clients, so the redemption inherits the user's IP.
My assumption (not yet tested) is that this block would occur on every token refresh as well, not just the initial sign-in, since the vendor's server would also be making those refresh requests as a public client.
How to Identify This
Check for non-interactive sign-ins where both:
- Client credential type = None, and
- the IP address doesn't match the user's corresponding interactive sign-in (e.g. it's an unknown or vendor IP address)
Neither condition is enough on its own. Native mobile and desktop apps (like Outlook or Teams) are public clients too, so they'll also show a client credential type of None, but from the user's own device and IP.
Device Signals Are Inherited Too
The device platform, device ID and Entra join state are inherited by the non-interactive sign-in for both client types. In my test, that cut both ways: the inherited Windows platform is part of what made the location policy apply, while the inherited hybrid joined state kept the same sign-in out of scope of a device-trust policy.
So device-based controls don't constrain where an authorization code is redeemed any more than location-based ones do for confidential clients. A policy requiring a managed or compliant device is evaluated against the user's device, not against the machine actually redeeming the code.
Fixes
Vendor side: I believe the best fix is for the vendor to configure their app as a confidential client: a Web platform redirect URI on their own infrastructure, with the authorization code redeemed using a client credential (ideally a certificate or federated credential rather than a client secret).
Customer side: At the time of writing, Anthropic's own setup instructions for the Microsoft 365 connector recommend excluding their IP range (160.79.104.0/21) from location-based policies. However, this is far from ideal. That range is shared by all of Anthropic's customers, so trusting it means anything originating from Anthropic's environment passes your location check, for any app and any user in your tenant, not just this connector. That poses a significant supply chain risk, whether Anthropic or one of its other customers is compromised, or another customer is simply acting maliciously.
Excluding the connector instead doesn't work here either: the connector needs access to Microsoft Graph, and excluding Graph from a location-based policy isn't a reasonable option. That leaves customers without a good option until there's a vendor-side fix.
Is This a Design Flaw?
In my opinion, this appears to be a design flaw on the vendor side. The authorization code is being redeemed on the vendor's servers, and OAuth 2.0 (RFC 6749, section 2.1) classifies clients by whether they can keep a credential confidential; an application running on a web server is the spec's own example of a confidential client. The OAuth 2.0 Security Best Current Practice (RFC 9700) also recommends using client authentication wherever possible. So while it doesn't strictly break the framework, redeeming the code as a public client from a server runs against how the framework is intended to be used. I am not set on this, however, and am interested in any opposing views.
A Note on Confidential Clients
For confidential clients, the IP address logged in the non-interactive sign-in log is not where that request actually came from.
- SOC analysts and incident responders should be aware of this when tracing authentications for OAuth confidential clients.
- Location-based Conditional Access policies cannot restrict where authorization codes are redeemed for confidential clients. That's by design: for confidential clients, the redemption is protected by the client credential rather than by location.
What's Next
The main loose end left is confirming exactly how the authorization code makes its way to the vendor's servers. I'll update this post once I have answers. If you've seen different behaviour or disagree with any of the above, I'd love to hear from you.
Related Reading
- Conditional Access Resource Exclusions: Remediation + SIEM, on how resource exclusions in Conditional Access policies really behave and how to find the apps they affect.
- Limiting Token Lifetimes in Entra ID, on refresh tokens, sign-in frequency and Continuous Access Evaluation.




