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.
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.
Before the right answer, feel why the obvious answers are wrong. Teams cycle through three of them, usually in this order:
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.
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.
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.
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.
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.
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.
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.
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.
--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.
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:
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.
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.
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.
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:
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.
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.
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.
Six rabbit holes the main argument stepped around. Each stands alone.
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.roles claim inside a Graph token carries these; an ARM token has no equivalent because ARM checks RBAC live instead.exp./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.