Microsoft Entra Applications, Managed Identities, and Workload Authentication
This section gives you an administrator-friendly model for understanding modern workload identity in Microsoft Entra. The goal is not to turn you into an OAuth developer. The goal is to help you evaluate application identities, reason about risk, inspect tokens, and choose stronger authentication methods for automation.
Figure: Legacy service account reality - passwords spread into scripts, configs, and CI logs
Traditional service accounts were usually ordinary directory identities with passwords. They enabled compatibility, but they also created repeatable weaknesses:
The cloud version of the same pattern is still risky. A non-human account with a password and a permanent security exception is still a high-value target.
Many organizations migrated their on-premises service account patterns directly to the cloud: app registrations with client secrets, no expiry enforcement, Mail.ReadWrite on every mailbox, and Conditional Access exclusions. This is the same legacy weakness in a new location. The sessions ahead will show you how to find and fix these patterns.
Each generation of managed service account tightened the link between identity and the hosts allowed to use it:
Service Account Evolution Timeline
| Type | Host Binding | Password Rotation | Storage | Introduced |
|---|---|---|---|---|
| sMSA (Standalone) | Single host only | Automatic - every 30 days by default | AD, managed by the local DC | Windows Server 2008 R2 |
| gMSA (Group) | Multiple approved hosts via security group | Automatic - every 30 days, distributed via KDS root key | AD, shared through Key Distribution Services | Windows Server 2012 |
| dMSA (Delegated) | Credential-bound to a specific approved execution context | Automatic - credential material rotated and bound via Credential Guard | AD, integrated with Credential Guard on the host | Windows Server 2025 |
These are not identical to Entra service principals, but they help explain the direction: less password handling and stronger control over how a workload identity is used.
A gMSA lets any approved host retrieve the same credential. A dMSA binds the credential to a specific machine's Credential Guard, so even if an attacker compromises another approved host, the credential material is not usable. dMSA also supports migrating existing service accounts without changing passwords, because credential flow is handled at the platform level. Requires Windows Server 2025 domain functional level.
On-premises patterns have direct Entra counterparts.
Mapping Old Concepts to Cloud Concepts
The minimum model to remember:
One global definition, multiple tenant-local instances.
Identity Map
Workload identity federation eliminates secrets for external workloads by establishing an OIDC trust between Entra ID and the external identity provider. The flow works like this:
Supported external IdP platforms include GitHub Actions, Azure Kubernetes Service, Amazon EKS, Google Cloud, and any platform that publishes OIDC discovery metadata. The trust configuration specifies the issuer URI and a subject identifier that scopes which workloads can authenticate (e.g., a specific GitHub repo and branch).
Two views of the same application - different objects, different admin tasks.
App Registrations vs Enterprise Applications
Figure: App registration instantiates one Service Principal per consuming tenant; shared App ID, unique Object IDs
App registrations and enterprise applications are not duplicates.
Important identifiers:
Good rule:
When you register an app, you get one Application (client) ID that travels everywhere. But the application object (in the home tenant) and each service principal (in every consuming tenant) each have their own object ID. API calls that target the application definition use the application object ID. API calls that target local governance - permissions, sign-ins, Conditional Access - use the service principal object ID. Using the wrong object ID is the most common Graph API troubleshooting mistake in this area.
Multitenant apps extend the attack surface across organizational boundaries.
Single-Tenant vs Multitenant Risk
The common operational ranking from weakest to strongest:
| Method | Secret Handling | Lifecycle | Rotation Burden | Risk Level |
|---|---|---|---|---|
| Client secret | Plaintext shared secret stored in Entra and copied to the workload | Manual expiry (configurable, max 2 years via policy) | Admin must rotate before expiry or service breaks | High - replayable, easy to leak in logs, repos, and config files |
| Certificate | Asymmetric - only the public key stored in Entra; private key stays with the workload | Manual certificate renewal | Admin manages the full cert lifecycle (issue, deploy, renew) | Medium - private key must be secured, but not replayable like a secret |
| Federated credential | No stored secret - OIDC token exchange at runtime | Managed by the external IdP | None - trust-based, no credential material to rotate | Low - no credential to steal; attacker must compromise the external IdP |
| Managed identity | No credential ever exists in any form accessible to admins or code | Fully platform-managed by Azure | None - Azure handles everything | Lowest - no credential surface at all |
Client secrets appear in: source control commits, pipeline logs, shared OneNote pages, email threads, browser local storage, and Key Vault access policies that are too broad. Rotating the secret after a leak is necessary but insufficient - you must also revoke active tokens and audit what was accessed during the exposure window.
Auth Ladder
Figure: Secrets are still passwords - client secrets leak into CI logs, screenshots, and repos
Figure: Certificate lifecycle demands secure key storage, rollover planning, and expiration monitoring
Figure: Federated credential flow - OIDC token exchange with no stored workload secret
Figure: Managed identity - Azure platform manages the credential lifecycle; least privilege still required
Three distinct tokens - each with a different audience and purpose.
Token Types in Delegated Scenarios
Figure: App-only token - roles present, scp absent, no user context, no refresh token
The two token models differ in who is acting and what claims appear:
| Aspect | Delegated (on-behalf-of user) | App-only (client credentials) |
|---|---|---|
| User context | User signs in; app acts on behalf of the user | No user context - the app acts as itself |
| Permission claim | scp (scope) - lists delegated permissions | roles - lists application permissions |
| Absent claim | roles is typically absent | scp should be absent |
| Refresh tokens | May be issued depending on flow and settings | Not issued - client credentials flow does not use refresh tokens |
| Effective permissions | Intersection of delegated permissions granted AND user's own permissions | Exactly the application permissions granted via admin consent |
| Risk profile | Scoped by the signed-in user's access level | No user ceiling - blast radius equals the full set of granted app permissions |
A valid access token proves the identity authenticated successfully, but the token's roles or scp claims determine what the identity is authorized to do. A 401 means the token itself is invalid or missing. A 403 means the token is valid but lacks the required permission. When troubleshooting, always check the token claims first.
Token Anatomy
Permissions and consent matter first:
Each permission type widens the scope of what the app can reach.
Permissions and Consent Define Blast Radius
Application management policies let you restrict credential types and lifetimes across all apps or specific apps in the tenant:
Application management policies only block new credential creation - they do not retroactively remove existing secrets or certificates. If you deploy a 90-day max lifetime policy, any secret created before the policy with a 2-year expiry continues working until it expires. You must audit existing credentials separately. Also note that these policies do not apply to SAML signing certificates, which have their own lifecycle through the enterprise application.
App instance lock can harden sensitive enterprise app instances by preventing changes to specific properties (like redirect URIs and credential configuration) unless the change originates from the app registration in the home tenant. This protects multi-tenant apps where the consuming tenant should not be able to alter the app's security-critical settings.
Workload identity Conditional Access requires Entra Workload ID Premium licensing and is narrower than user CA. It supports location conditions and service principal risk, but it does not support device compliance, app filters, session controls, or MFA (which cannot apply to non-interactive flows). Also: a managed identity with broad permissions and no workload identity CA policy is still dangerous - the identity doesn't need to be a service principal with a secret to cause damage.
Control Matrix
Figure: Sign-in logs - validate resource, application, result, and IP details
Figure: Exchange Sharepoint
Do not assume every Microsoft 365 workload behaves the same way. Both Exchange Online and SharePoint Online have their own authorization layers on top of Entra identity.
Figure: Exchange Online case study
New-ApplicationAccessPolicy to restrict which mailboxes an app could access - this still works but Microsoft recommends migrating to App RBAC for new implementationsSites.Selected scopes app access to specific SharePoint sitesFigure: SharePoint Online case study
When an app-only call to Exchange or SharePoint fails, check two places: the service principal sign-in logs in Entra (to confirm the token was issued) and the workload-specific admin center (to confirm the role or permission assignment). A successful Entra sign-in with a 403 from the workload means the Entra side is fine but the workload-level authorization is missing.
Figure: Both retirements complete - SP-less auth (March 31) and SharePoint ACS (April 2, 2026)
Have PowerShell, Azure CLI, tenant access, and the provided app/token samples ready. If the live tenant path is unavailable, use the prepared sample token and screenshots to identify the same objects and claims. Clean up any disposable app, secret, and permission grant created during the lab.
Create a single-tenant app registration, inspect the related enterprise application, grant a low-risk Microsoft Graph application permission, request an app-only token, decode the token, and use it to call Microsoft Graph.
Run the REST-based setup steps in the same terminal session so the admin token stays available in $accessToken:
$tenantId = "<tenant-id>"
az login --tenant $tenantId
$accessToken = az account get-access-token --resource-type ms-graph --tenant $tenantId --query accessToken -o tsv
# Can copy this value to jwt.ms to inspect the admin token claims if desired
$accessToken
Entra ID -> App registrations.New registration.Session1-GraphReader-<initials>.Single tenant only.Register.Record:
Managed application in local directory.You should now have:
Certificates & secrets.New client secret.Native PowerShell:
# Replace with your application object ID (NOT the client/app ID) $appObjectId = "<application-object-id>" $secretEndDateTime = (Get-Date).ToUniversalTime().AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ") $body = @{ passwordCredential = @{ displayName = "Lab secret" endDateTime = $secretEndDateTime } } | ConvertTo-Json $secret = Invoke-RestMethod ` -Method Post ` -Uri "https://graph.microsoft.com/v1.0/applications/$appObjectId/addPassword" ` -Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } ` -Body $body # Save the secret value - it is only shown once $clientSecret = $secret.secretText $clientSecret
Graph PowerShell:
Connect-MgGraph -Scopes "Application.ReadWrite.All" # Replace with your application object ID (same value used in /applications/{id}) $appObjectId = "<application-object-id>" $secretEndDateTime = (Get-Date).ToUniversalTime().AddDays(1) $secret = Add-MgApplicationPassword -ApplicationId $appObjectId -PasswordCredential @{ DisplayName = "Lab secret" EndDateTime = $secretEndDateTime } # Save the secret value - it is only shown once $clientSecret = $secret.SecretText $clientSecret
Az CLI:
# Replace with your application object ID or appId $appObjectId = "<application-object-id>" $secretEndDateTime = (Get-Date).ToUniversalTime().AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ") $clientSecret = az ad app credential reset ` --id $appObjectId ` --append ` --display-name "Lab secret" ` --end-date $secretEndDateTime ` --query password ` --output tsv $clientSecret
This lab uses a client secret for convenience. In production, a client secret combined with broad application permissions (like Mail.ReadWrite for all mailboxes) recreates the same risk pattern as legacy service accounts with passwords. Prefer managed identities or federated credentials whenever possible.
Reminder:
API permissionsMicrosoft GraphApplication permissionsOrganization.Read.All# Step 4a: Add Organization.Read.All to required resource access
$appObjectId = "<application-object-id>"
# Microsoft Graph service principal appId is always 00000003-0000-0000-c000-000000000000
# Organization.Read.All role ID: 498476ce-e0fe-48b0-b801-37ba7e2685c6
$body = @{
requiredResourceAccess = @(
@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = @(
@{
id = "498476ce-e0fe-48b0-b801-37ba7e2685c6"
type = "Role"
}
)
}
)
} | ConvertTo-Json -Depth 4
Invoke-RestMethod `
-Method Patch `
-Uri "https://graph.microsoft.com/v1.0/applications/$appObjectId" `
-Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } `
-Body $body
# Step 4b: Grant admin consent (create appRoleAssignment on the service principal)
$spObjectId = "<service-principal-object-id>"
# Get the Microsoft Graph service principal in your tenant
$graphSp = Invoke-RestMethod `
-Method Get `
-Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'" `
-Headers @{ Authorization = "Bearer $accessToken" }
$graphSpId = $graphSp.value[0].id
$consentBody = @{
principalId = $spObjectId
resourceId = $graphSpId
appRoleId = "498476ce-e0fe-48b0-b801-37ba7e2685c6"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spObjectId/appRoleAssignments" `
-Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } `
-Body $consentBody
# Step 4a: Add Organization.Read.All to required resource access
$appObjectId = "<application-object-id>"
# Microsoft Graph service principal appId is always 00000003-0000-0000-c000-000000000000
# Organization.Read.All role ID: 498476ce-e0fe-48b0-b801-37ba7e2685c6
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# Replace with your application object ID
$appObjectId = "<application-object-id>"
$params = @{
requiredResourceAccess = @(
@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = @(
@{
id = "498476ce-e0fe-48b0-b801-37ba7e2685c6"
type = "Role"
}
)
}
)
}
Update-MgApplication -ApplicationId $appObjectId -BodyParameter $params
# Step 4b: Grant admin consent (create appRoleAssignment on the service principal)
$spObjectId = "<service-principal-object-id>"
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All"
# Resolve the Microsoft Graph service principal object ID in this tenant
$graphSpId = Get-MgServicePrincipal `
-Filter "appId eq '00000003-0000-0000-c000-000000000000'" |
Select-Object -First 1 -ExpandProperty Id
$params = @{
principalId = $spObjectId
resourceId = $graphSpId
appRoleId = "498476ce-e0fe-48b0-b801-37ba7e2685c6"
}
New-MgServicePrincipalAppRoleAssignment `
-ServicePrincipalId $spObjectId `
-BodyParameter $params
# Step 4a: Add Organization.Read.All to required resource access
$appObjectId = "<application-object-id>"
# Microsoft Graph service principal appId is always 00000003-0000-0000-c000-000000000000
# Organization.Read.All role ID: 498476ce-e0fe-48b0-b801-37ba7e2685c6
az ad app permission add `
--id $appObjectId `
--api 00000003-0000-0000-c000-000000000000 `
--api-permissions 498476ce-e0fe-48b0-b801-37ba7e2685c6=Role
# Step 4b: Grant admin consent for the configured app permissions
# Reuse the application object ID from step 4a.
# This grants admin consent for all permissions currently configured on the app registration.
az ad app permission admin-consent `
--id $appObjectId
$tenantId = "<tenant-id>"
$clientId = "<application-client-id>"
$clientSecret = "<client-secret-value>"
$tokenResponse = Invoke-RestMethod `
-Method Post `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body @{
client_id = $clientId
client_secret = $clientSecret
scope = "https://graph.microsoft.com/.default"
grant_type = "client_credentials"
}
$accessToken = $tokenResponse.access_token
$tenantId = "<tenant-id>"
$clientId = "<application-client-id>"
$clientSecret = "<client-secret-value>"
$secureClientSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$clientSecretCredential = New-Object System.Management.Automation.PSCredential($clientId, $secureClientSecret)
Connect-MgGraph `
-TenantId $tenantId `
-ClientId $clientId `
-ClientSecretCredential $clientSecretCredential `
-NoWelcome
$tenantId = "<tenant-id>"
$clientId = "<application-client-id>"
$clientSecret = "<client-secret-value>"
az login `
--service-principal `
--username $clientId `
--password $clientSecret `
--tenant $tenantId
$accessToken = az account get-access-token `
--resource-type ms-graph `
--tenant $tenantId `
--query accessToken `
--output tsv
$accessToken
$tokenParts = $accessToken.Split(".")
$payload = $tokenParts[1].Replace("-", "+").Replace("_", "/")
switch ($payload.Length % 4) {
2 { $payload += "==" }
3 { $payload += "=" }
}
$claimsJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload))
$claims = $claimsJson | ConvertFrom-Json
$claims | Format-List aud, tid, appid, roles, scp, exp
Expected observations:
appid matches the app registrationroles includes Organization.Read.Allscp is empty or missing$headers = @{ Authorization = "Bearer $accessToken" }
$response = Invoke-RestMethod `
-Method Get `
-Uri "https://graph.microsoft.com/v1.0/organization" `
-Headers $headers
$response
$response.value
Open service principal sign-ins in the Entra admin center and look for the app-only sign-in to Microsoft Graph.
Entra ID โ Monitoring & health โ Sign-in logs.Service principal sign-ins tab.Microsoft Graph.Service principal sign-in logs are separate from user sign-in logs. When troubleshooting app-only access failures, always check this tab first. The log entry shows whether the token was issued, what resource was targeted, and whether any Conditional Access policies were evaluated. If the sign-in succeeded but the API call returned 403, the problem is in the permission grant, not the authentication.
Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:
All links below were reviewed on 2026-03-06. Microsoft Learn and Microsoft documentation are the primary sources. Community and MVP posts are included only where they add operational nuance.
Section 1 - Understanding Modern Service Principals - Reference Guide