interactive explainer · azure identity · oauth

Machines don't log in.
They authenticate.

A service principal is how software gets its own identity in Azure: its own passport, its own permissions, its own blast radius. This page builds one from scratch, decodes the token it receives, then hands you the slider that decides how bad a leaked-credential day gets. At the end, a full playground.

Part I · chapters 01 to 03

Who's asking?

01 · the incident

The deploy bot logs in as Priya

A gut-check before anything else. Count the automated things at your company that touch a cloud API every night: pipelines, cost reports, backups, sync jobs. Now count how many of them have their own identity. The gap between those numbers is this page.

Priya is a data engineer at a retail company. Her Tuesday task: a nightly script that reads yesterday's blob files from a storage account, aggregates spend, and writes a report. The script needs to authenticate to Azure, so she does what nearly every beginner does. She makes it sign in the way she signs in: her username, her password, parked in an environment variable on the build server, because a script cannot type.

It works. It works for four months. Then IT enables mandatory multi-factor authentication, and at 2:07 a.m. her script sits at a login prompt with no phone and no thumb. The cost report never runs. Neither do the six other jobs that were also, quietly, being Priya.

Nothing was hacked. Nobody meant harm. The system did what it was built to do, which was to treat every login as a person. That assumption is the bug.

02 · the workarounds

Every password workaround fails a different way

Before the right answer, feel why the obvious answers are wrong. Teams cycle through three of them, usually in this order:

the workaround graveyard
Figure 1Click each workaround to see its specific failure. All three share one root cause.

Look at the shape of the failures. A credential built for a person, with a person's lifecycle, a person's MFA, a person's sprawling permissions, is being worn by a program. The costume never fits. The script does not need a borrowed password. It needs to be somebody.

03 · the concept

Give the software its own passport

The fix is older than Azure: OAuth 2.0. Programs stop carrying human passwords. Instead, a central authority (in Azure, Microsoft Entra ID) issues short-lived, signed access tokens. A program proves who it is once, receives a token good for about an hour, and presents that token with every API call. No password ever travels to the API.

OAuth has several ceremonies for different situations. When a human is present to click Approve, that is the authorization-code flow. When no human exists, as in Priya's nightly script, it is the client credentials flow: the application authenticates as itself.

And the somebody it becomes has a name. Registering an app in Entra ID creates an app registration and, from it, a service principal: a real identity object in your tenant. It can hold roles. It appears in audit logs. One click disables it during an incident. Hover the terms in this paragraph, or the nodes below, to see how the pieces connect the night everything works. The token is the only thing the storage account ever sees.

the cast, one nightly run
identity objects tokens + authority resources credentials
hover a node or an edge to inspect it
Figure 2Hover any node or edge to inspect it. These five colors mean the same thing in every figure on this page.
A service principal is a user account for something that will never forget its password and never pass MFA.
Part II · chapters 04 to 08

The machinery

04 · smallest working version

Four fields buy a token

Create the identity first. One CLI command replaced Priya's password:

# app registration + service principal + secret + role, in one shot
az ad sp create-for-rbac \
  --name "sp-nightly-costs" \
  --role "Storage Blob Data Reader" \
  --scopes /subscriptions/<sub-id>/resourceGroups/rg-finance/providers/Microsoft.Storage/storageAccounts/stfinreports

Then, at 2 a.m., the script trades its credential for a token. A plain HTTPS POST, four form fields, no SDK magic:

POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token

grant_type    = client_credentials
client_id     = a1b2c3d4-…          # who I claim to be
client_secret = wxy8Q~…              # proof of the claim
scope         = https://management.azure.com/.default   # which API this token is for

Every Azure SDK in every language does exactly this underneath. Run the healthy exchange, then run the three classic ways it breaks. The error codes are real; you will meet them.

token exchange · pick a scenario
Figure 3Click a scenario to replay the exchange. Note where each failure surfaces: three at the token endpoint, one later, at the API itself.

The wrong-audience case deserves a second look. The token endpoint happily issues a token for Microsoft Graph, and the exchange succeeds. The failure arrives later, when ARM refuses it. Where an error surfaces tells you which layer rejected you, and chapter 09 maps all of them.

05 · inside the token

The token confesses everything

An access token looks like line noise. It is three base64 segments joined by dots, a JWT: header.claims.signature. Nothing in it is encrypted. Anyone holding it can read it, and you should: the claims name which API it is for, which identity it represents, how that identity proved itself, and when it dies. Its power lives entirely in the signature, which only Entra ID can produce.

jwt inspector · encoded and decoded live by this page
click a claim, or hover the dotted terms in the paragraph above
Figure 4Click any claim row for what it means and why security engineers care. The page encodes this token itself: readable, yes; forgeable, no.

One absence matters as much as any claim: for a service principal calling ARM there are no permissions inside the token. Azure looks those up fresh on every call, which is what makes chapter 07's slider work the way it does.

06 · the confusing trio

One app, three faces

The portal shows three different things for what feels like one identity, and it trips up everyone in their first month. The split earns its keep the moment an app serves more than one tenant. The Azure CLI itself is one blueprint in Microsoft's tenant, yet a distinct service principal of it lives in yours, in your customer's, in everyone's. Hover the figure: the appId is identical everywhere, the oid never is.

one blueprint, one principal per tenant
hover the app object or any service principal
Figure 5Hover any node. Same appId in every tenant; a different oid, and therefore different permissions, in each.
Figure 6Click each face for what it is and which portal blade owns it.
07 · the knob

Scope is the blast radius

A service principal with no roles can authenticate all day and do nothing. Identity and permission are separate systems. Permissions come from Azure RBAC, and a role assignment is a triple: who (the principal's oid), what (a role; here it stays fixed at Contributor), and where (a scope). Scope inherits downward. Assign at a subscription and you have assigned to everything inside it, including resources that do not exist yet.

The demo tenant: 174 resources, 3 subscriptions, one management group. The gold dot is Priya's storage account. Drag the scope and watch three views of the same assignment move together: the tree it covers, the dots it can touch, and the exact CLI flag that created it.

role assignment · role fixed: contributor
resources this identity can modify or delete1 / 174

      
Figure 7One slider, three synced representations: the scope tree (covered branches in pink), the resource dots, and the --scopes flag as the CLI would take it.

The job needs one resource. Every dot past the first is unearned permission: pure downside, waiting. Yet subscription-level Contributor stays the most common grant in real tenants, because it is the one that never produces a 403 support ticket. Least privilege means choosing the smallest scope that lets the job run, then accepting that you will widen it occasionally, on purpose, in writing.

Nobody ever got paged for a role that was too narrow. The wide ones page you years later, all at once.
08 · the impact

A leaked secret is an afternoon. Whose depends on chapter 07

Make it concrete. The secret leaks: pushed to a public repo, echoed into a CI log, lifted from the build server. The attacker holds client_id, client_secret, and your tenant id, which was never secret to begin with. One POST later (the same four fields from Figure 3) they hold a valid token. Here is their afternoon, at whatever scope you left the slider on:

attacker's afternoon · contributor @ resource
Figure 8This panel tracks Figure 7's slider. Scroll up, move it, come back.

Two details worth sitting with. Tokens are bearer tokens: whoever bears one, is you, with no second check. And deleting the leaked secret does not kill tokens already issued; they ride until their exp, up to an hour of "we fixed it but it is not fixed." The blast radius you chose in chapter 07 is exactly the size of this list.

What Contributor could not do at any scope: grant roles (Owner or User Access Administrator territory) or read Key Vault secrets, because data-plane access is a separate assignment. Azure's control-plane and data-plane split has quietly saved a lot of incidents. Chapter 09 shows where it lives.
Part III · chapters 09 to 11

The nitty-gritty

09 · production architecture

Every API call re-litigates your permissions

The full path of one nightly-job request, and it runs on every call, not once per session. Each layer can reject you, and each rejects differently: a 401 means the token itself failed, a 403 means the token was fine and RBAC said no. That distinction is the first thing to check in any access incident.

request path · click a layer
Figure 9Click each layer for what it checks and how it says no. Hovering the 401/403 phrases in the paragraph highlights the layer that produces each.

The design principle hiding in the stack: the API never sees a secret. The credential buys tokens at layer 2 and nowhere else. Layers 4 to 6 deal only in short-lived, signed, auditable claims. A compromised API server, a logged request, a sloppy proxy: none of them ever held the thing that mints access.

10 · limitations and failure modes

The best secret is no secret

Everything so far assumed a client secret, and the client secret is the weakest thing on this page: a password by another name, generated rather than chosen, still copyable, loggable, committable. Azure offers a ladder of four ways for an identity to prove itself. Each rung removes a failure mode and adds a constraint:

credential ladder · weakest to strongest
Figure 10Click each rung. The playground below lets you feel what each choice does to the blast radius.

And the honest list of what no rung fixes:

Sprawl. Service principals never resign. Tenants accumulate hundreds: ownerless, still permissioned, secrets expiring silently or, worse, not expiring. Nothing inventories them for you by default.

No MFA, ever. That is the whole point of a service principal, so the identity protections built around humans apply weakly or not at all. Compensate with credential hygiene and narrow scope, not with hope.

Revocation lag. Kill a secret, disable a principal: issued tokens still ride until exp. Up to an hour where the fix has shipped and the door is still open.

The audit gap. Sign-in logs say a principal authenticated. Activity logs say what it did. Correlating them across tools at 2 a.m. is a skill of its own, and narrow scope is what keeps the answer boring.

11 · in the wild

How the grown-ups deploy

Assemble the whole page into the setup you will meet at a well-run company, say a bank's platform team shipping through GitHub Actions.

CI/CD uses federation, not secrets. The pipeline authenticates with workload identity federation, rung four of Figure 10: GitHub's own OIDC token, exchanged for an Entra token, trusted only from one repo and one branch. Nothing stored, nothing to rotate, nothing to leak. For new automation outside Azure this is the starting point, not the secret from create-for-rbac.

Workloads inside Azure use managed identities. The nightly job now runs in an Azure Function with a system-assigned identity holding Storage Blob Data Reader on one storage account: the leftmost stop on Figure 7's slider. Priya's password has not been near it in years.

The surviving secrets live in Key Vault, for the third-party SaaS that still demands one, with expiry alerts and rotation runbooks. The appidacr claim from Figure 4 gives auditors the list of apps still authenticating with secrets, reviewed quarterly next to an inventory of every assignment wider than Reader.

Why a bank pays for the ceremony: auditors ask two questions. Who can touch production, and prove it. With named principals, narrow scopes, federated credentials, and claims in every log line, both answers are queries. With shared passwords, both answers are meetings.

Identity answers who's asking.
The token proves it for an hour.
Scope decides how much that hour can cost you.

playground

Now break it yourself

The guided part is over. The argument held the role and the credential constant so one slider could carry chapter 07. Here every control is exposed. Set up an identity, then read what a leak of it would mean. The presets rebuild the configurations this page has been arguing about.

identity workbench · demo tenant, 174 resources
scope
resources readable
resources writable
can grant roles to others
stored credentials to steal
if leaked right now
Figure 11Three controls, live consequences. Try the bank preset, then give it Owner at management group and watch the readout argue with you.
into the weeds

For the readers who scrolled this far

Six rabbit holes the main argument stepped around. Each stands alone.

Managed identities are service principals underneath
List service principals with az ad sp list and you will find your managed identities among them, with servicePrincipalType: ManagedIdentity. Same identity machinery, same RBAC binding, same token claims. The difference is custody: the platform owns the credential and refuses to show it to anyone, including you. Everything this page says about scope and tokens applies to them unchanged.
Calling Microsoft Graph needs app roles, not RBAC
Azure RBAC governs ARM: subscriptions, resource groups, resources. Microsoft Graph (users, groups, mail, Teams) has a separate model: application permissions, granted on the app registration and consented by an admin. A principal can be Owner of every subscription and still unable to list users. The roles claim inside a Graph token carries these; an ARM token has no equivalent because ARM checks RBAC live instead.
Deny assignments and PIM
Two refinements on the RBAC story. Deny assignments override allows and are mostly created by Azure itself (Blueprints, managed apps) to protect resources from their own owners. Privileged Identity Management makes role assignments just-in-time: the assignment exists but stays inactive until someone activates it, with approval and an expiry. For service principals, PIM support is narrower than for humans; scope discipline does the heavy lifting.
Token caching, and the 429 you earn without it
A token is valid for about an hour; asking for a new one per request multiplies your token-endpoint traffic by thousands and eventually meets a throttle. MSAL and the Azure SDKs cache tokens per resource per identity and refresh them shortly before expiry. If you hand-roll the POST from chapter 04, cache the result keyed on (tenant, client, scope) and reuse it until a minute before exp.
v1 versus v2 tokens
Two endpoint generations coexist. The v2 endpoint (/oauth2/v2.0/token) takes a scope parameter; v1 took resource. Claim names drift too: v1 says appid, v2 says azp; issuer URLs differ. Validation libraries handle both, but hand-written claim checks that grep for the wrong name fail quietly on the other generation. The ver claim in Figure 4 exists so code can branch.
ROPC, the flow you will inherit and should retire
Resource Owner Password Credentials: the app sends a username and password straight to the token endpoint. It is the OAuth flow that looks most like Priya's original sin, which is why old scripts use it. It breaks on MFA, cannot see conditional access properly, and Microsoft documents it as legacy. Finding ROPC in a codebase is finding a migration ticket.