Conditional Access vs service principals: which sign-in rules apply
For developers wiring up automation against Microsoft Entra ID / Azure AD, the confusing part is not creating an app registration; it is predicting which Conditional Access controls will fire for a human, a managed identity, or a service principal. This guide explains the decision path, shows one realistic token flow end to end, and gives you practical commands and error patterns to diagnose policy mismatches quickly.
TL;DR — Conditional Access primarily evaluates interactive and workload identities differently. The most common mistake is assuming the same MFA, device, or location rules that block a user sign-in will also block a service principal token request; they usually do not, unless you explicitly target workload identities with policies that support them.
Reading time: ~7 min
What it is and where it sits
When people say "Conditional Access blocks this app," they often mix together three different actors:
- a user signing in interactively
- a service principal using client credentials or certificate auth
- a managed identity acting as a workload identity inside cloud infrastructure
The practical question is: which policy engine evaluates which token request?
In a typical Entra ID / OAuth 2.0 flow, Conditional Access sits in the token issuance path at the identity provider. It does not live in your app, your reverse proxy, or your API gateway. Your app sees the result later as claims in the token, a failed token request, or a claims challenge.
What it replaces, operationally, is the old habit of relying only on static credentials and coarse tenant-wide settings. Instead of "all users can sign in if they know the password," you get policy decisions based on identity type, app, network, device state, risk, authentication strength, and sometimes workload identity conditions.
Architecture-wise, the important split is this:
- User sign-in: browser/device/app talks to the authorization endpoint; Conditional Access can require MFA, compliant device, phishing-resistant auth, trusted network, sign-in frequency, etc.
- Service principal sign-in: daemon/CI job/backend talks to the token endpoint using client secret or certificate; no human is present, so user-centric controls like MFA and device compliance do not apply.
- Managed identity sign-in: cloud runtime gets a token from the platform metadata endpoint, then exchanges/uses it against Entra-backed resources; again, no user/device interaction.
[Developer laptop / Browser]
|
| interactive auth code / device code
v
[Identity Provider: Entra token service]
|
| Conditional Access for user sign-in
v
[Access token with CA-related claims] ---> [API / Graph / custom resource]
[CI runner / daemon / backend]
|
| client_credentials + secret/cert
v
[Identity Provider: Entra token service]
|
| workload identity evaluation (if policy targets it)
v
[Access token or token error] -----------> [API / Graph / custom resource]
The key context: service principals are enterprise representations of applications, and their sign-ins are recorded separately from user sign-ins. If you are debugging "why did MFA not trigger?" the answer is usually that there was no user sign-in in the first place.
How it actually works
Take one realistic example: a GitHub Actions runner or self-hosted CI job calls Microsoft Graph using a service principal and client certificate.
Step-by-step flow
- You register an application and create its service principal in the tenant.
- You grant it application permissions to Graph, for example
User.Read.AllorDirectory.Read.All, and admin-consent them. - Your CI job requests a token from the tenant token endpoint using
grant_type=client_credentials. - Entra identifies the caller as a workload identity, not a user.
- Conditional Access evaluates only the policy types that apply to workload identities. User-only controls such as:
- Require MFA
- Require compliant device
- Sign-in frequency
- Terms of use do not make sense here and are not enforced the same way.
- If there is a workload identity Conditional Access policy targeting that service principal and the conditions match, token issuance can be allowed, blocked, or constrained according to supported controls.
- If allowed, the token is issued with app identity claims such as
appid,azp,roles, and tenant identifiers. You will not see user claims likeupnbecause there is no user. - Graph receives the token and authorizes based on the app permissions in the token and the resource-side checks.
Concrete token request
curl -sS -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "scope=https://graph.microsoft.com/.default" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$CLIENT_ASSERTION_JWT"
Successful response shape:
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiLCJub25jZSI6I..."
}
Then call Graph:
curl -sS https://graph.microsoft.com/v1.0/users?$top=1 \
-H "Authorization: Bearer $ACCESS_TOKEN"
What failure looks like
If a workload identity policy blocks the sign-in, the token request fails before your app sees anything useful.
Typical shape:
{
"error": "invalid_grant",
"error_description": "AADSTS53003: Access has been blocked by Conditional Access policies. The access policy does not allow token issuance. Trace ID: 7d3b... Correlation ID: 8f1a... Timestamp: 2026-10-01 09:14:22Z",
"error_codes": [53003],
"timestamp": "2026-10-01 09:14:22Z",
"trace_id": "7d3b...",
"correlation_id": "8f1a...",
"error_uri": "https://login.microsoftonline.com/error?code=53003"
}
If you expected a user MFA prompt but used client credentials, you will not get one. You will just get a token or a hard failure. There is no browser challenge to complete because no user session exists.
The practical rule set
For decision-making, think in this order:
- What grant type is being used?
authorization_codeanddevice_codeimply a user.client_credentialsimplies app-only. - Is there a user in the token? If not, user-targeted Conditional Access controls are irrelevant.
- Is the policy scoped to workload identities? If not, service principal sign-ins are outside that policy.
- Does the target resource do extra checks? Graph, your API, or downstream services may still reject the token based on app permissions, claims, tenant restrictions, or custom authorization.
When to use it (and when not to)
Use Conditional Access for service principals when you need policy over non-human identities, especially high-privilege automation. Do not expect it to be a substitute for permission hygiene, secret rotation, or network controls.
| Scenario | Recommendation |
|---|---|
| Human admins using portal, CLI, or browser apps | Use user-targeted Conditional Access: MFA, auth strength, device/risk controls |
| CI/CD pipeline calling Graph or your API with app-only auth | Use workload identity Conditional Access if available in your tenant/provider; also restrict app permissions hard |
| Azure-hosted app using managed identity to reach storage/Key Vault/API | Prefer managed identity over client secrets; use resource RBAC first, then add workload identity policy if needed |
| Legacy daemon using a long-lived client secret from a VM | Migrate to certificate auth or managed identity before adding more policy |
| You want MFA on a service principal | You probably do not need this because MFA is a human control; redesign the auth flow |
| You want to know "why was token issuance denied?" | Check sign-in logs for workload identities first, not user sign-in logs |
You probably do not need workload-targeted Conditional Access if:
- the app has only low-risk read access and already runs under managed identity with narrow RBAC
- your real problem is overbroad app permissions like
Directory.ReadWrite.All - you are trying to compensate for leaked secrets instead of removing secrets
- the caller is your own backend inside a private network and resource-side authorization already gives you the control you need
Trade-offs
Every benefit here has a cost.
-
Benefit: central policy over automation identities
Cost: another policy layer to debug. A 401 from the resource and anAADSTS53003from token issuance mean very different things. -
Benefit: reduce blast radius for compromised service principals
Cost: rollout risk. A bad policy can break CI/CD, Terraform, deployment agents, and background jobs instantly. -
Benefit: better governance for privileged app identities
Cost: inventory burden. You need to know which service principal maps to which repo, runner, environment, and owner. -
Benefit: fewer exceptions for "headless" workloads
Cost: feature asymmetry. Not every user-centric Conditional Access control has a workload equivalent. -
Benefit: stronger posture when paired with certificates or federated credentials
Cost: operational complexity. Certificate rollover, federation trust setup, and log correlation are more work than a client secret in an env var. -
Benefit: policy-driven deny instead of relying only on code discipline
Cost: vendor coupling. Conditional Access behavior is identity-provider-specific, even though OAuth grant types are standard.
In practice
Example 1: Inspect whether a token is app-only before blaming Conditional Access
ACCESS_TOKEN="$(jq -r .access_token token.json)"
python3 - <<'PY'
import os, json, base64
jwt = os.environ['ACCESS_TOKEN'].split('.')
payload = jwt[1] + '=' * (-len(jwt[1]) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload))
for k in ['appid','azp','roles','scp','upn','oid','sub','tid']:
if k in claims:
print(f"{k}: {claims[k]}")
PY
This prints the token claims that tell you whether the call is app-only (roles, appid, no upn) or delegated (scp, often user claims). Gotcha: this decodes the token without verifying the signature; use it only for local diagnosis, not trust decisions.
Typical output for app-only:
appid: 11111111-2222-3333-4444-555555555555
roles: ['User.Read.All']
oid: 66666666-7777-8888-9999-aaaaaaaaaaaa
tid: bbbbbbbb-cccc-dddd-eeee-ffffffffffff
sub: 66666666-7777-8888-9999-aaaaaaaaaaaa
If you do not see scp and there is no upn, stop expecting user MFA or device compliance policy to apply.
Example 2: Request a token with Azure CLI as a service principal and capture the exact failure
⚠️ This command changes the current Azure CLI login context on the machine where you run it. On shared admin workstations or CI runners, run it in an isolated shell/session to avoid breaking parallel jobs.
az login --service-principal \
--username "$CLIENT_ID" \
--tenant "$TENANT_ID" \
--password "$CLIENT_SECRET"
az account get-access-token \
--resource-type ms-graph \
--query '{tenant:tenant, expiresOn:expiresOn, tokenType:tokenType}' \
-o json
What it does: authenticates as the service principal, then asks Azure CLI to fetch a Graph token so you can separate credential problems from API permission problems. Gotcha: if Conditional Access blocks workload sign-in, az login itself may fail before any token is returned.
Typical blocked output shape:
ERROR: AADSTS53003: Access has been blocked by Conditional Access policies. The access policy does not allow token issuance.
Trace ID: 7d3b1c2d-....
Correlation ID: 8f1a9e0b-....
Timestamp: 2026-10-01 09:14:22Z
Typical bad-secret output shape, which is a different problem entirely:
ERROR: AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID.
Example 3: Prefer certificate-based client auth over a shared secret
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
-keyout sp-key.pem \
-out sp-cert.pem \
-subj "/CN=ci-workload-sp"
openssl x509 -in sp-cert.pem -fingerprint -sha256 -noout
This creates a private key and self-signed certificate you can upload to the app registration and use for client assertion auth. Gotcha: the private key file is now a production credential; store it in a secret manager or, better, move to federated workload identity so there is no long-lived private key on disk.
Further reading
- Microsoft Entra Conditional Access documentation, especially the workload identities section
- Microsoft identity platform OAuth 2.0 client credentials grant flow
- Microsoft Graph permissions reference
- OAuth 2.0 Authorization Framework (RFC 6749)
- JSON Web Token (JWT) (RFC 7519)
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI