Securing Applications and Automation - Training Series
Section 1 of 6
Reference Guide

Understanding Modern Service Principals

Microsoft Entra Applications, Managed Identities, and Workload Authentication


Section
Section 1 - Understanding Modern Service Principals
Document Type
Reference Guide
Format
Instructor-led with guided lab
Audience
Infrastructure and security administrators

Section purpose

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.

Learning Objectives

Legacy service accounts: what problem are we solving?

password123 svc_deploy C:\tasks\ creds.txt Admin group: "just in case" CI log line 847: SECRET=p4ssw0rd credentials scattered everywhere AD model + internet exposure = risk

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.

โš ๏ธRisk - The Cloud Version of This Problem

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.

MSA evolution as a bridge to cloud identity

Each generation of managed service account tightened the link between identity and the hosts allowed to use it:

ON-PREMISES CLOUD Standard SA Password, manual sMSA 1 host, Auto-rotate Server 2008 R2 gMSA Multi-host, Auto-rotate Server 2012+ dMSA Stronger host binding Server 2025 SP Secret Stored cred, rotate SP Cert Better assurance, manage lifecycle WIF / Federated No stored secret Managed Identity No credential ever exists

Service Account Evolution Timeline

TypeHost BindingPassword RotationStorageIntroduced
sMSA (Standalone)Single host onlyAutomatic - every 30 days by defaultAD, managed by the local DCWindows Server 2008 R2
gMSA (Group)Multiple approved hosts via security groupAutomatic - every 30 days, distributed via KDS root keyAD, shared through Key Distribution ServicesWindows Server 2012
dMSA (Delegated)Credential-bound to a specific approved execution contextAutomatic - credential material rotated and bound via Credential GuardAD, integrated with Credential Guard on the hostWindows 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.

๐Ÿ“˜Key Concept - dMSA vs gMSA

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.

Mapping old concepts to cloud concepts

On-premises patterns have direct Entra counterparts.

ON-PREMISES / LEGACY ENTRA / CLOUD Service Account + Password Manual rotation, shared credentials Service Principal + Client Secret Still a replayable credential - plan to move off sMSA (Standalone MSA) Auto-rotate, bound to one host System-Assigned Managed Identity Tied to one Azure resource lifecycle gMSA (Group MSA) Multi-host, 30-day auto-rotation User-Assigned Managed Identity Shared across multiple Azure resources Cross-Domain Trust / Keytab Forest trusts, stored keytab files Workload Identity Federation No stored secret - OIDC trust with external IdP No Equivalent On-prem never had this assurance level Azure Managed Identity No credential ever stored or rotated

Mapping Old Concepts to Cloud Concepts

Modern Entra workload identities

The minimum model to remember:

One global definition, multiple tenant-local instances.

App Registration(Home tenant - one per app) instantiates Service Principal(per tenant - local object) admin view Enterprise Application(permissions, sign-ins, CA) Managed IdentityAzure-managed SP lifecycle special case of SP App (Client) ID: Same in App Reg and Enterprise App Object IDs differ between the two objects

Identity Map

Federated credentials in detail

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:

  1. The external workload (e.g. a GitHub Actions runner) obtains a token from its own platform IdP.
  2. The workload presents that token to the Microsoft identity platform token endpoint.
  3. Entra validates the token against the configured federated credential (checking issuer, subject, and audience).
  4. If validation passes, Entra issues an access token with no client secret or certificate involved.

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).

App registrations, enterprise applications, and IDs

Two views of the same application - different objects, different admin tasks.

App Registration HOME TENANT - GLOBAL DEFINITION ๐Ÿ”‘ Redirect URIs ๐Ÿ‘ฅ Supported account types ๐Ÿ” Secrets & certificates ๐Ÿ“ก Expose an API (scopes, app roles) ๐Ÿ“‹ API permissions (requested) Object ID = Application Object ID Enterprise Application LOCAL TENANT - SERVICE PRINCIPAL VIEW ๐Ÿ‘ค User/group assignment ๐Ÿ“Š Sign-in logs & audit ๐Ÿ›ก๏ธ Conditional Access evaluation โœ… Permissions (granted / consented) โš™๏ธ Provisioning & ownership Object ID = Service Principal Object ID

App Registrations vs Enterprise Applications

App Registration Home Tenant Application (Client) ID shared everywhere Service Principal Home tenant Object ID: abc-001 Service Principal Partner tenant Object ID: def-002 Same App ID ยท Different Object IDs One app definition โ†’ many SP instances

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:

๐Ÿ“˜Key Concept - Same App ID, Different Object IDs

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.

Single-tenant versus multitenant

Multitenant apps extend the attack surface across organizational boundaries.

SINGLE-TENANT YOUR TENANT App Registration (definition) Service Principal (local instance) โœ… Trust stays inside one boundary MULTITENANT VENDOR TENANT (home) App Registration (vendor controls this) CUSTOMER TENANT A SP (local) CUSTOMER TENANT B SP (local) consent consent โš  Vendor compromise โ†’ customer impact Every tenant with an SP is in the blast radius Mitigations for multitenant risk: โ–ธ Workload identity Conditional Access โ–ธ Limit consent to verified publishers โ–ธ Require admin consent for all permissions โ–ธ Monitor SP sign-in logs for anomalies

Single-Tenant vs Multitenant Risk

Authentication method ladder

The common operational ranking from weakest to strongest:

MethodSecret HandlingLifecycleRotation BurdenRisk Level
Client secretPlaintext shared secret stored in Entra and copied to the workloadManual expiry (configurable, max 2 years via policy)Admin must rotate before expiry or service breaksHigh - replayable, easy to leak in logs, repos, and config files
CertificateAsymmetric - only the public key stored in Entra; private key stays with the workloadManual certificate renewalAdmin manages the full cert lifecycle (issue, deploy, renew)Medium - private key must be secured, but not replayable like a secret
Federated credentialNo stored secret - OIDC token exchange at runtimeManaged by the external IdPNone - trust-based, no credential material to rotateLow - no credential to steal; attacker must compromise the external IdP
Managed identityNo credential ever exists in any form accessible to admins or codeFully platform-managed by AzureNone - Azure handles everythingLowest - no credential surface at all
โš ๏ธRisk - Secrets Leak in Predictable Places

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.

Managed Identity No credential ever stored or rotated - platform-managed lifecycle STRONGEST Workload Identity Federation No stored secret - OIDC trust with external IdP LOW RISK Certificate Better assurance - recommends PKI MEDIUM Client Secret Replayable - easy to leak, hard to reliably rotate HIGH RISK Weakest Strongest

Auth Ladder

Client secret

CLIENT_SECRET=xK9#mP2... CI Log Screenshot Local notes Sample code GitHub repo replayable - once leaked, always leaked

Figure: Secrets are still passwords - client secrets leak into CI logs, screenshots, and repos

Certificate

๐Ÿ”‘ Private Key X.509 Certificate Subject: CN=MyApp Valid to: 2026-12-01 Signed assertion โœ“ Key Secure Storage Rollover Plan Before Expiry Expiry Monitor Alerting Better than a secret string but not credential-free

Figure: Certificate lifecycle demands secure key storage, rollover planning, and expiration monitoring

Federated credential

GitHub Actions Entra ID Token STS JWT token โ†’ โ† Entra token Federated Trust Config issuer ยท subject ยท audience CLIENT_SECRET no stored secret in the workload

Figure: Federated credential flow - OIDC token exchange with no stored workload secret

Managed identity

Azure VM Workload Azure Platform Manages credential lifecycle Entra ID Managed SP credential-free token request โ†’ Caveats โ€ข Still needs least-privilege RBAC assignments โ€ข CA coverage narrower than user sign-ins โ€ข Best for Azure-hosted workloads only

Figure: Managed identity - Azure platform manages the credential lifecycle; least privilege still required

Token basics for administrators

Delegated scenario

Three distinct tokens - each with a different audience and purpose.

User signs in Client App e.g. PowerShell Entra ID issues tokens Resource API e.g. Graph auth code exchange ID Token Refresh Token Access Token Purpose: Proves who signed in to the client app - never for APIs Key claims: sub ยท name ยท oid ยท tid preferred_username Audience: Client App only โ†’ stays in client Purpose: Silently renews access tokens without re-prompting the user Claims: Opaque - no readable claims Long-lived; protect carefully Note: Absent in client_credentials โ€’ โ€’ โ†’ renews access token โ†’ Purpose: Bearer token authorizing calls to the resource API Key claims: aud ยท scp (delegated scopes) roles ยท oid ยท exp ยท appid Audience: Resource API (aud claim) โ†’ sent to Resource API

Token Types in Delegated Scenarios

JWT Access Token (app-only) roles: ["Org.Read.All"] โœ“ scp: (absent) oid: service principal object ID No user No refresh token roles claim = what the app can do

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:

AspectDelegated (on-behalf-of user)App-only (client credentials)
User contextUser signs in; app acts on behalf of the userNo user context - the app acts as itself
Permission claimscp (scope) - lists delegated permissionsroles - lists application permissions
Absent claimroles is typically absentscp should be absent
Refresh tokensMay be issued depending on flow and settingsNot issued - client credentials flow does not use refresh tokens
Effective permissionsIntersection of delegated permissions granted AND user's own permissionsExactly the application permissions granted via admin consent
Risk profileScoped by the signed-in user's access levelNo user ceiling - blast radius equals the full set of granted app permissions
๐Ÿ“˜Key Concept - Authentication โ‰  Authorization

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.

Header alg: RS256typ: JWT . Payload (Claims) aud: https://graph.microsoft.com/iss: https://sts.windows.net/...oid: (service principal object ID)roles: ["Organization.Read.All"]scp: (absent in app-only)exp: 1741916400 (~1 hour) . Signature RSASHA256(base64(header)+"."+ base64(payload),privateKey) Tells the receiver howto verify the token The actual data. Inspect roles or scpto diagnose 403 permission errors Prevents tampering.Verified by Entra's public key

Token Anatomy

Governance controls

Permissions and consent matter first:

Each permission type widens the scope of what the app can reach.

APPLICATION PERMISSIONS Entire tenant - no user context - all mailboxes, all files, all users DELEGATED + ADMIN CONSENT All users can exercise the permission through the app DELEGATED + USER CONSENT Only the signed-in user's data AUTHENTICATION Proves identity only - no data access Examples Mail.Read (delegated) One user's mailbox only User.Read.All (delegated) Any user through the app Mail.ReadWrite.All (app) Every mailbox in the tenant โš  Admin consent is the gate: โ–ธ One admin click can grant tenant-wide capability โ–ธ Review what you're consenting to

Permissions and Consent Define Blast Radius

Application management policies

Application management policies let you restrict credential types and lifetimes across all apps or specific apps in the tenant:

โš ๏ธRisk - Policy Failure Modes

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

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.

Conditional Access for workload identities

โš ๏ธRisk - Workload Identity CA Misconceptions

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.

ControlUser AccountService PrincipalManaged Identity Multi-Factor AuthenticationConditional Access (user policy)Workload ID Conditional AccessPrivileged Identity ManagementSign-in Logs โœ… Fully supportedโœ— Not applicableโœ— Not applicable โœ… Governs all sign-insโœ— Delegated sign-ins onlyโœ— Not covered n/aโœ… Supported (Premium)~ Limited support โœ… Roles and groupsโœ… Azure RBAC rolesโœ… Azure RBAC roles โœ… Interactive + non-interactiveโœ… Service principal sign-insโœ… Managed identity sign-ins

Control Matrix

Sign-in logging and investigations

Time Application Resource Status IP Address 09:00:09 BackupSvc MS Graph Success 10.0.4.22 10:00:11 BackupSvc MS Graph Success 10.0.4.22 10:14:33 BackupSvc MS Graph Failure 185.22.3.5 11:00:07 BackupSvc MS Graph Success 10.0.4.22 11:00:08 BackupSvc Key Vault Success 10.0.4.22 12:00:04 BackupSvc MS Graph Success 10.0.4.22 โ† investigate

Figure: Sign-in logs - validate resource, application, result, and IP details

Exchange Online and SharePoint Online nuance

Exchange Online Entra Auth Token Exchange RBAC Role Both required - token + role assignment Use Exchange App RBAC or legacy App Access Policy New: EWS Application Access Policy โ†’ prefer App RBAC SharePoint Online Entra App-Only Sites.Selected scope Preferred: Entra app + certificate auth Use Sites.Selected to limit site access scope Azure ACS app-only RETIRED April 2, 2026

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.

Exchange Online

Entra ID Issues access token for EXO roles: [Mail.Read, etc.] token accepted Exchange Online Exchange-side RBAC check App RBAC roles โ‰  Entra roles App RBAC Modern path Preferred design App Access Policy Legacy path Avoid in new designs Token issuance โ‰  authorization EXO RBAC enforces separately

Figure: Exchange Online case study

SharePoint Online

SharePoint Online Entra App-only + Certificate Auth Azure ACS app-only RETIRED Apr 2026 Sites.Selected scope Azure ACS retired April 2, 2026

Figure: SharePoint Online case study

๐Ÿ’กPro Tip - Sign-in Log Troubleshooting

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.

9. Current-state deadlines and admin takeaways

March 31, 2026 SP-less Authentication โœ“ RETIRED App-only flows now require a linked service principal April 2, 2026 SharePoint ACS App-Only โœ“ RETIRED Use Entra app-only with certificate authentication Mar 31 Apr 2 TODAY Impact if not yet migrated SP-less auth patterns are now blocked by Entra ID SharePoint ACS token issuance has stopped Check sign-in logs for failed auth requests Migrate remaining workloads to Entra app registrations

Figure: Both retirements complete - SP-less auth (March 31) and SharePoint ACS (April 2, 2026)

Lab readiness notes

๐ŸงชLab Readiness

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.

Guided lab

Lab goal

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.

Step 0: Sign in and get an admin Microsoft Graph token

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 Portal:

Step 1: Register the app
  1. Go to Entra ID -> App registrations.
  2. Select New registration.
  3. Name the app Session1-GraphReader-<initials>.
  4. Set supported account types to Single tenant only.
  5. Select Register.

Record:

Step 2: Open the managed application
  1. In the app registration, select Managed application in local directory.
  2. Record the enterprise application / service principal object ID.

You should now have:

Step 3: Create a client secret
  1. Open Certificates & secrets.
  2. Select New client secret.
  3. Copy the secret value when it is displayed.

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
โš ๏ธRisk - Secret + Broad Permissions = Legacy Weakness

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:

Step 4: Add Microsoft Graph application permission

Entra Portal:

  1. Open API permissions
  2. Add Microsoft Graph
  3. Choose Application permissions
  4. Search and add Organization.Read.All
  5. Grant admin consent

Native PowerShell:

# 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

Graph PowerShell:

# 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

Az CLI:

# 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

Step 5: Request the token

Native PowerShell:

$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

Graph PowerShell:

$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

Az CLI:

$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

Step 6: Decode the token

$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:

Step 7: Call Microsoft Graph

$headers = @{ Authorization = "Bearer $accessToken" }
$response = Invoke-RestMethod `
  -Method Get `
  -Uri "https://graph.microsoft.com/v1.0/organization" `
  -Headers $headers

$response

$response.value

Step 8: Review the sign-in evidence

Open service principal sign-ins in the Entra admin center and look for the app-only sign-in to Microsoft Graph.

  1. Go to Entra ID โ†’ Monitoring & health โ†’ Sign-in logs.
  2. Switch to the Service principal sign-ins tab.
  3. Find the sign-in for your app - look for the application name and a resource of Microsoft Graph.
  4. Inspect the details: note the IP address, location, and the authentication method used (client secret).
๐Ÿ’กPro Tip - Sign-in Logs Are Your First Stop

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.

Quick recap questions

  1. Why is a client secret closer to a password than to a managed identity?
  2. What is the difference between the application object ID and the service principal object ID?
  3. Which claim do you expect in a delegated access token that should not appear in an app-only token?
  4. Why does client credentials flow not use refresh tokens?
  5. Why should Exchange Online and SharePoint Online be taught as different workload stories?

Key reminders

Current source notes

Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:

References

References Appendix

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.

Microsoft documentation

Community and MVP references

Section 1 - Understanding Modern Service Principals - Reference Guide