As administrators, we’ve all been there: a command fails with a “403 Forbidden” error, and the message says we don’t have the necessary role. You check the Microsoft Entra admin center, and there it is, staring back at you in plain sight: Global Administrator.
So, why is the command still failing? This is the story of a deep-seated token issue and how a little bit of PowerShell forensics can save you from pulling your hair out.
The Initial Problem: A Global Administrator is Denied
I was trying to run a simple PowerShell command using the Microsoft Graph SDK to list conditional access policies:
Get-MgIdentityConditionalAccessPolicy -All
The error message was frustratingly clear, yet completely wrong:
Your account does not have access to this report or data. One of the following roles is required: Security Reader, ... Global Reader, ... Company Administrator.
I had the Company Administrator
role (Global Administrator). My initial troubleshooting steps were the obvious ones:
- Check the Scope: I confirmed my
Connect-MgGraph
command included the requiredPolicy.Read.All
scope. - Check the Role: I verified in the admin center that my account had the Global Administrator role.
- Wait for PIM: My organization uses Privileged Identity Management (PIM), so I ensured the role was activated and waited for propagation.
None of it worked. The error persisted, suggesting the issue was deeper than a simple misconfiguration.
The Breakthrough: The Token Is the Final Authority
I had to shift my thinking. It wasn’t about what the admin center said my roles were; it was about what the access token said they were. The access token is the temporary credential issued to the PowerShell session, and it’s the final authority that the Microsoft Graph API uses for authorization.
The solution was to inspect the token itself. Since it’s a secure credential, we can’t just view it, but we can retrieve it and decode its contents.
Step 1: Retrieving the Microsoft Graph Access Token
The Microsoft Graph PowerShell SDK doesn’t have a direct cmdlet to show the token, but we can make a simple request and extract it from the HTTP response headers.
# First, connect to Microsoft Graph with your credentials and scopes.
Connect-MgGraph -Scopes "Policy.Read.All"
# Next, make a simple request to a non-sensitive endpoint,
# specifying the -OutputType as HttpResponseMessage.
$response = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/me" -OutputType HttpResponseMessage
# Finally, extract the access token from the Authorization header.
$mgToken = $response.RequestMessage.Headers.Authorization.Parameter
# Display the token string (it will be very long).
Write-Host "Microsoft Graph Token:"
$mgToken
Step 2: Decoding the Token Locally
To view the token’s contents, you can decode it locally using PowerShell. This keeps your secure credential on your machine.
# Use the token string retrieved in Step 1.
# $mgToken = ...
# Split the token into its three parts (header, payload, and signature).
$tokenParts = $mgToken.Split('.')
$payload = $tokenParts[1]
# Pad the payload with '=' characters to ensure it's a valid Base64 string.
while ($payload.Length % 4 -ne 0) {
$payload += '='
}
# Decode the payload from Base64, convert to a string, and then to a PowerShell object.
$decodedPayload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload)) | ConvertFrom-Json
# Display the decoded object in a readable format.
Write-Host "Decoded Microsoft Graph Token Payload:"
$decodedPayload | Format-List
The Smoking Gun: The Missing Global Administrator GUID
Upon inspecting the decoded payload, I found something shocking. The token had a claim called wids
(Windows Identity System) which lists the GUIDs of all built-in directory roles for the user.
For a Global Administrator, you would expect to see two GUIDs: the standard “User” role GUID and the Global Administrator GUID (62e90394-69f5-4237-9190-012177145e14
).
However, my wids
claim only contained one:
wids : {b79fbf4d-3ef9-4689-8143-76b194e85509}
This GUID, b79fbf4d-3ef9-4689-8143-76b194e85509
, is the unique identifier for a standard user account. My token had no idea I was a Global Administrator. The 403 Forbidden
error was correct from the API’s perspective because my token was invalid for the action I was trying to perform.
Conclusion: The Token Propagation Delay
The core issue was a token propagation delay. Even though my role was assigned and active in the Entra ID admin center, that change had not yet fully synchronized across all of Microsoft’s services to the system that issues the access tokens for the Microsoft Graph API.
The final takeaway is clear:
- A
403 Forbidden
error, even for a Global Administrator, can be a symptom of an outdated access token. - The token, not the portal, is the source of truth for your permissions.
- The solution is to wait for the synchronization to complete and a new, correct token to be issued on your next login.
Update: Got it working!
I wrote this article on a Friday evening, presumably when Entra was not propagating tokens correctly. I tested this a few days later and now my wids
contains the expected roles. Proving, that Entra was the issue here, not me!
wids : {
f2ef992c-3afb-46b9-b7cf-a126ee74c451, # Global Reader
62e90394-69f5-4237-9190-012177145e10, # Global Administrator
b79fbf4d-3ef9-4689-8143-76b194e85509 # standard user account
}
Bonus Section: The Exchange Online Paradox
In the midst of this frustration, I discovered a strange paradox: I could still successfully run PowerShell commands for Exchange Online. This suggested that a separate token was being used.
My hypothesis is that the admin role had already propagated to the system that issues tokens for the Exchange Online API, while it was still delayed for Microsoft Graph.
Here is the code I used to try and retrieve that token, a test I plan to revisit at a later time.
# Install the MSAL.PS module to get tokens directly from Azure AD
Install-Module -Name MSAL.PS -Scope CurrentUser
# Define the Exchange Online PowerShell client ID and your tenant ID
$exchangeOnlineClientId = "97f394c8-3162-435e-a346-601262d18401"
$tenantId = (Get-MgContext).TenantId
# Request a new token for the Exchange Online resource.
# This will prompt you to authenticate in a browser.
$exchangeToken = Get-MsalToken -TenantId $tenantId -ClientId $exchangeOnlineClientId | Select-Object -ExpandProperty AccessToken
# Decode the token using the local script from above
# (Note: This step failed during the original troubleshooting due to a different tenant configuration error,
# which is a story for another blog post!)
$tokenParts = $exchangeToken.Split('.')
$payload = $tokenParts[1]
while ($payload.Length % 4 -ne 0) {
$payload += '='
}
$decodedPayload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload)) | ConvertFrom-Json
Write-Host "Decoded Exchange Online Token Payload:"
$decodedPayload | Format-List