A post on my TechStack blog about Azure Key Vault answered the question of where secrets live. This one answers a different, equally fundamental question: how does one application actually prove its identity to another and stay proven over time without constant re-authentication. Managing exactly this - a three-month token refresh cycle for one external system, a six-month cycle for another - was a real operational responsibility on an Azure-based integration platform at Blue Yonder. This post explains the mechanism underneath that responsibility.
What Microsoft Entra ID Actually Is
Microsoft Entra ID, formerly Azure Active Directory and still commonly called Azure AD, is Microsoft's identity platform. It is where users, applications, and services prove who they are before being allowed to do anything at all. Every Managed Identity referenced in earlier posts on this blog is, underneath, an Entra ID identity using exactly the mechanism described here.
Think of a hotel. Checking in verifies identity once - a passport, a reservation, a form of payment. In exchange, the front desk issues a key card. The card itself does not prove who someone is; it proves they were verified recently and are permitted into specific rooms for a specific window of time. The card eventually expires. When it does, full check-in is not required again - showing the old card along with proof of still being a guest is enough to get a new one.
An access token is the key card. The identity verification at check-in is the OAuth flow itself. A refresh token is the proof shown at the desk to get a new card without redoing the full check-in process.
The Core Vocabulary
- The Resource Owner is the user, or in service-to-service contexts the system, that owns the data being accessed.
- The Client is the application requesting access - a web app, a backend service, a mobile app.
- The Authorization Server issues tokens after verifying identity, and in this context that role belongs to Entra ID.
- The Resource Server is the API that actually holds the requested data Salesforce's API, ServiceNow's API, or an internal API.
- An Access Token is short-lived proof of authorization, the key card itself, sent with every API request typically in the Authorization header. A Refresh Token is a longer-lived credential used to obtain a new access token without re-authenticating from scratch.
- Scope defines what a token is actually permitted to do - read access to a resource is a meaningfully different scope from read and write access to that same resource.
Client Credentials Flow
This is the flow that matters most for backend integrations - precisely the kind of system-to-system connection that powers an integration platform. No human is involved at any point. One application proves its own identity directly in order to gain access to another application system's API.
The application holds a Client ID and either a Client Secret or a certificate, registered in Entra ID ahead of time. It sends those credentials directly to the Entra ID token endpoint. Entra ID verifies the credentials and returns an access token - no user, no browser redirect, no login screen anywhere in this entire exchange. The application includes that access token in the Authorization header of every subsequent API call to the target system. When the access token expires, commonly after one hour, the application requests a new one using the same client credentials, and the cycle repeats automatically - usually handled transparently by a library rather than custom code.
In C#, using the Microsoft.Identity.Client (MSAL) NuGet package, this means building a ConfidentialClientApplication with the client ID, client secret, and tenant authority, then calling AcquireTokenForClient with the target scope. MSAL caches the resulting token automatically and silently refreshes it on subsequent calls if the cached token has expired - there is no need to write manual expiry-checking logic in application code.
Authorization Code Flow
This is the flow behind every "Sign in with Microsoft" button. Unlike client credentials, a real human user is present and must explicitly approve the requested access.
- A user clicks sign in, and the browser redirects to Entra ID's login page.
- The user authenticates - password, multi-factor authentication, whatever policy requires - and approves the requested scopes.
- Entra ID redirects back to the application with a short-lived authorization code, deliberately not a token yet.
- The application's backend then exchanges that code, along with its own client secret, for an access token and a refresh token.
- The access token handles subsequent API calls; when it expires, the refresh token obtains a new access token without requiring the user to log in again.
The reason the code-for-token exchange happens server-side, separately from the browser redirect, is a deliberate security measure. The authorization code passes through the user's browser and could theoretically be intercepted, but it is useless on its own without the client secret, which never leaves the application's backend.
Why Access Tokens Are Deliberately Short-Lived
If an access token leaks - logged accidentally, intercepted in transit, stored somewhere insecure - a short expiry limits the actual damage window. A typical access token lifetime is around one hour. A typical refresh token lifetime runs closer to ninety days, often configurable, and frequently with sliding expiration where each use resets the clock.
This is precisely why token refresh cycles become a genuine operational concern in production systems. A refresh token itself eventually expires too, and if nothing actively refreshes it before that happens, the integration breaks until someone manually re-authenticates from scratch. The three-month and six-month cycles managed at Blue Yonder for two different external systems were exactly this scenario - each external provider enforced its own refresh token lifetime, and centralizing those credentials in Key Vault meant there was one governed place to monitor expiry and rotate credentials proactively, rather than discovering an integration had silently broken.
What a JWT Actually Contains
Most Entra ID access tokens are JSON Web Tokens, a specific and inspectable format rather than a random opaque string.
A JWT consists of three parts separated by dots -
a header indicating which algorithm signed the token,
a payload containing the actual claims (who issued it, who it is intended for, what scopes it carries, when it expires), and
a signature providing cryptographic proof the token has not been tampered with.
A critical detail: JWTs are signed, not encrypted. The payload is fully readable by anyone holding the token, even without any ability to forge a new one. This is precisely why a JWT should never contain sensitive data directly in its claims - readability is not a bug in this format, it is the design and treating the payload as confidential is a genuine security mistake.
Validating a Token on the Receiving Side
Protecting an API endpoint with token validation in ASP.NET Core involves configuring JWT Bearer authentication with the expected authority and audience, and enabling validation of the issuer, audience, lifetime, and signing key. Once configured, an Authorize attribute on a controller action is enough to require a valid token - the middleware itself handles signature verification, expiry checking, issuer checking, and audience checking automatically none of which needs to be written by hand.
Refresh Token Rotation
Conceptually, refreshing an access token means sending a request to the token endpoint with a grant type of refresh token, the client credentials, and the previously stored refresh token. The response contains a new access token, and frequently a brand new refresh token as well - a pattern called refresh token rotation, where the old refresh token is invalidated the moment a new one is issued.
This rotation behavior is worth knowing about specifically, because it is a genuine, subtle source of production bugs. If a system stores a refresh token and the provider rotates it on every use, the stored copy must be updated after every single refresh. Skip that update once, and the next refresh attempt fails with an invalidated token - often discovered only when an integration that worked fine in testing quietly stops working in production weeks later.
Common Mistakes
Storing access tokens long-term defeats their entire security model - they are designed to be short-lived and re-fetched frequently, not cached for days.
Hardcoding a client secret directly in source code repeats the exact mistake covered in the Key Vault post on this blog - client secrets belong in Key Vault or GitHub Secrets, never committed alongside application code.
Failing to handle refresh token rotation correctly, as described above, causes integrations to fail unpredictably after their first successful refresh cycle.
Requesting overly broad scopes - asking for full read and write access when only read access is actually needed - is a real, avoidable security smell worth catching in code review.
Confusing authentication with authorization is a more conceptual mistake: a valid token proves who is asking, but it does not automatically mean they are permitted to do what they are asking. That second check is a separate concern, typically handled through scopes or application roles layered on top of basic token validity.
Choosing the Right Flow
Client Credentials Flow fits service-to-service scenarios with no human user present - backend integrations calling another backend API, scheduled jobs, daemon processes, and the Salesforce and ServiceNow style integrations referenced throughout this blog's other posts. Authorization Code Flow fits scenarios where a real human user needs to log in and the application acts on that specific user's behalf - any "sign in with Microsoft" style scenario.
Neither flow is more secure than the other in the abstract; they solve genuinely different problems. Using client credentials for a user-facing login, or authorization code for a headless backend job, are both signs of reaching for the wrong flow for the actual situation at hand.
How This Connects to the Rest of This Blog's Azure Coverage
Key Vault, covered in an earlier post, is where the client secret in this entire flow actually lives. Managed Identity, covered in that same post, is in fact a special case of this exact OAuth machinery - an Azure resource receives an automatically managed Entra ID identity, and the client credentials flow happens entirely behind the scenes with no secret for a developer to manage at all. Application Insights, covered in an even earlier post, is where a failed token refresh would actually surface in production - an exception appearing in the logs, a spike in 401 responses, exactly the kind of pattern a KQL query against the requests table would catch quickly.
Key Lessons
Client Credentials Flow is the right mental model for almost every backend-to-backend Azure integration - two systems proving identity to each other with no user involved anywhere in the exchange.
Refresh tokens exist specifically so a long-running integration does not require constant manual re-authentication, but they still expire, and that expiry genuinely needs active, ongoing monitoring.
Refresh token rotation is a frequent, subtle source of production bugs whenever a stored token is not updated on every single refresh.
A JWT's payload is readable by anyone holding the token - sensitive data should never be placed directly in token claims.
Managed Identity removes the need to manage a client secret at all by having Azure itself handle the client credentials flow transparently on a resource's behalf.
Requesting the minimum scope an integration genuinely needs, rather than the broadest available scope, is a simple and frequently overlooked security improvement.
Summary
OAuth 2.0 is the mechanism underneath nearly every system-to-system connection in enterprise Azure architecture.
Client Credentials Flow handles service-to-service trust with no user involved at all.
Authorization Code Flow handles a real user logging in and delegating access.
Access tokens are short-lived by design; refresh tokens exist to renew them quietly behind the scenes, and both carry real expiry windows that demand active management in any long-running integration.
Understanding this properly is what separates simply knowing an API call works from actually understanding why it works - and why it eventually stops working if nobody is watching the token lifecycle underneath it.
Originally published at TechStack Blog:
https://www.techstackblog.com/post.html?slug=azure-ad-oauth-token-flows-explained
Related reading on TechStack Blog:
https://www.techstackblog.com/post.html?slug=azure-key-vault-secrets-management
More from TechStack Blog, by category:
Azure: https://www.techstackblog.com/category.html?cat=azure
C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
Follow for weekly posts on Azure, C#, and cloud engineering.
Top comments (0)