OAuth2 and OIDC with Entra ID are a weekend in a monolith. Add a JWT bearer scheme, register an app, ship. The story collapses the moment the system splits into eight services that need to call each other, audit who initiated the request, and survive a token revocation without a global logout. The architectural decisions made in the first sprint set the ceiling for the next two years.
Sharing the user token across services is the trap most teams fall into. The frontend acquires a token for the gateway, the gateway forwards it to service A, service A forwards it to service B. Now every service has to accept the same audience, the blast radius of a leaked token is the entire platform, and refresh logic lives in places it has no business living. Do not do this. Each protected API gets its own audience and its own scope.
The pattern that holds up is a gateway that validates the user token, performs claims transformation, and reissues an internal token signed by a private authority that only the internal services trust. The internal token carries a stable subject, the original tenant, the originating request id, and the minimal scopes required by the call chain. Services validate the internal audience and stop worrying about Entra-specific concerns. The gateway is the only place that talks to Entra ID for user flows.
Propagation is the next decision. A DelegatingHandler attached to the typed HttpClient injects the internal token and the correlation id on every outbound call, so service-to-service hops never lose context. The correlation id ties together a trace in Application Insights or whichever OpenTelemetry backend you run, and the same id surfaces in audit logs. Without that handler, every team writes their own propagation and one of them gets it wrong.
Machine-to-machine calls go through the client credentials flow with managed identity wherever Azure allows it, never with a static client secret in a config file. A worker that publishes events or hits an internal API gets its own service principal, its own scope, and shows up in the audit log as itself. The temptation to reuse the gateway identity for background jobs is real and should be rejected. Identity confusion in logs is what makes incidents take six hours instead of one.
Two loops to avoid. The first is calling back into the same protected API that started the chain, which causes audience confusion and infinite recursion under retry. The second is refreshing the user token inside a downstream service, which couples that service to the identity provider for no reason. Refresh belongs at the edge, in the gateway or in the SPA. Internal tokens are short-lived and reissued, never refreshed. Get those two boundaries right and the platform stops fighting itself.
Tags
- #dotnet
- #azure
- #security
- #entra-id