Automating Azure PIM role activation secured with FIDO2/Passkey with PowerShell
Or any other Azure service requiring special authentication strength

If you invoke some Azure or Graph Api call and see an error similar to this one (RoleAssignmentRequestAcrsValidationFailed) ๐, you should read this post ๐.

The error code RoleAssignmentRequestAcrsValidationFailed, followed by the failed claim claims=%7B%22access_token%22%3A%7B%22acrs%22%3A%7B%22essential%22%3Atrue%2C%20%22value%22%3A%22c1%22%7D%7D%7D (unescaped it looks like this claims={"access_token":{"acrs":{"essential":true, "value":"c1"}}}) means the method used for authenticating the current session isnโt sufficient to run the selected action.
For me, it was when I activated the PIM-protected Azure Directory Role that required FIDO2 (Passkey) authentication strength (enforced via Azure Conditional Access Policy).

Anyway, if something like this happens, you need to reauthenticate using the failed claim and repeat the action.
How to authenticate to Azure/Graph with PowerShell using a specific claim
How to authenticate using a specific claim slightly differs based on the accessed service.
Making an Azure connection
We need to extract the claim from the
Invoke-AzRestMethodresponse bodyUnescape the HTML string and convert it to
base64Use such a claim in the
Claimsparameter of theConnect-AzAccountto fulfill interactively auth requirements
# invoke some request that needs extra claim (FIDO2)
$response = Invoke-AzRestMethod -Uri $restUri -Method PUT -Payload ($requestBody | ConvertTo-Json -Depth 10)
# when PIM requires FIDO/Passkey or specific MFA, the API returns 400 with a claims challenge
$exception = ($response.Content | ConvertFrom-Json).error.message
# reauthenticate using claim challenge
if ($exception -and $exception -match 'claims=([^"]+)') {
$claimsChallenge = $matches[1]
$claimsChallenge = [System.Uri]::UnescapeDataString($claimsChallenge)
Write-Verbose "Claims challenge detected: $claimsChallenge"
# convert plaintext claim to base64
$bytes = [System.Text.Encoding]::ASCII.GetBytes($claimsChallenge)
$encodedClaimsChallenge = [Convert]::ToBase64String($bytes)
#TIP tenant parameter is required to avoid error: WARNING: Unable to acquire token for tenant '<tenantid>' with error 'InteractiveBrowserCredential authentication failed: Redirect Uri mismatch. Expected (/favicon.ico) Actual (/). '
$null = Connect-AzAccount -Claims $encodedClaimsChallenge -Tenant (Get-AzContext).tenant.id -ErrorAction Stop -WarningAction SilentlyContinue
}
# now you can retry failed request
Write-Verbose "Retrying activation request..."
$response = Invoke-AzRestMethod -Uri $restUri -Method PUT -Payload ($requestBody | ConvertTo-Json -Depth 10)
Making a Graph connection (with default scopes)
Itโs very similar to making just an Azure connection, with a slight difference. The claim is saved in the error message instead of the response body (when using
Invoke-MgGraphRequest)And you need to reauthenticate to the Graph api after new access token retrieval
try {
$uri = "v1.0/roleManagement/directory/roleAssignmentScheduleRequests"
$response = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body -ErrorAction Stop
} catch {
# When PIM requires FIDO/Passkey or specific MFA, the API returns 403 with a claims challenge.
$exception = $_.ErrorDetails.Message
# reauthenticate using claim challenge
if ($exception -and $exception -match 'claims=([^"]+)') {
$claimsChallenge = $matches[1]
$claimsChallenge = [System.Uri]::UnescapeDataString($claimsChallenge)
Write-Verbose "Claims challenge detected: $claimsChallenge"
# convert plaintext claim to base64
$bytes = [System.Text.Encoding]::ASCII.GetBytes($claimsChallenge)
$encodedClaimsChallenge = [Convert]::ToBase64String($bytes)
#TIP tenant parameter is required to avoid error: WARNING: Unable to acquire token for tenant '<tenantid>' with error 'InteractiveBrowserCredential authentication failed: Redirect Uri mismatch. Expected (/favicon.ico) Actual (/). '
$null = Connect-AzAccount -Claims $encodedClaimsChallenge -Tenant (Get-AzContext).tenant.id -ErrorAction Stop -WarningAction SilentlyContinue
}
# Re-connect Graph SDK using the Strong Token
$secureToken = (Get-AzAccessToken -ResourceTypeName MSGraph).Token
Connect-MgGraph -AccessToken $secureToken -NoWelcome
Write-Verbose "Retrying activation request..."
$response = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body -ErrorAction Stop
}
Making a Graph connection (with specific scopes)
It again starts with the Azure reauthentication
But because we have to specify custom Graph api scopes, we cannot use
Connect-AzAccountto retrieve the access tokenInstead, we will need:
A list of required Graph Api scopes (
$scope)- (
RoleAssignmentSchedule.ReadWrite.Directoryto activate the PIM role)
- (
To import the Microsoft.Identity.Client.dll library to be able to create a web request with the required scopes using
[Microsoft.Identity.Client.PublicClientApplicationBuilder]To have a claim JSON string (
$claim)can look like
'{"access_token":{"acrs":{"essential":true, "value":"c1"}}}'c1is the ID of the authentication context defined in Azure Conditional Access policies
So, to authenticate using the specified claim, use Get-PIMGraphTokenWithClaim function from my AzurePIMStuff module.
$claimsChallenge = "<claim>"
# create new auth token
$secureToken = Get-PIMGraphTokenWithClaim -Claim $claimsChallenge
# re-connect Graph SDK using the Strong Token
Connect-MgGraph -AccessToken $secureToken -NoWelcome
Or if you need a different kind of scopes, etc, create the request using the following code
#region prepare
$clientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e" # "Microsoft Graph PowerShell" application ID
$scope = "<list of required scopes>"
# for actiovation of the PIM roles you will require "RoleAssignmentSchedule.ReadWrite.Directory"
$scope += "RoleAssignmentSchedule.ReadWrite.Directory"
# acrs == Authentication context
# c1 == id of the authentication context defined in Azure Conditional Access policies
$claim = '{"access_token":{"acrs":{"essential":true, "value":"c1"}}}'
#endregion prepare
#region authenticate using given claims
# Load Microsoft.Identity.Client.dll Assembly ([Microsoft.Identity.Client.PublicClientApplicationBuilder] type) from Az.Accounts module
if (!("Microsoft.Identity.Client.PublicClientApplicationBuilder" -as [Type])) {
# to avoid dll conflicts try loaded modules first
$modulePath = (Get-Module Az.Accounts | Sort-Object Version -Descending | Select-Object -First 1).ModuleBase
if (!$modulePath) {
$modulePath = (Get-Module Az.Accounts -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).ModuleBase
}
if ($modulePath) {
$dllPath = Get-ChildItem -Path $modulePath -Filter "Microsoft.Identity.Client.dll" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($dllPath) {
Add-Type -Path $dllPath.FullName
} else {
throw "Microsoft.Identity.Client.dll not found in Az.Accounts module."
}
} else {
throw "No Az.Accounts module found. Please install the Az PowerShell module."
}
}
# Build Public Client App
$pca = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($clientId).WithAuthority("https://login.microsoftonline.com/$tenantId").WithRedirectUri("http://localhost").Build()
# Create Interactive Request
$request = $pca.AcquireTokenInteractive($scope)
$request = $request.WithClaims($claim)
# Execute and return Token
$token = $request.ExecuteAsync().GetAwaiter().GetResult().AccessToken
$secureToken = ConvertTo-SecureString $token -AsPlainText -Force
#endregion authenticate using given claims
# Re-connect Graph SDK using the Strong Token
Connect-MgGraph -AccessToken $secureToken -NoWelcome
How to retrieve the claim
How to retrieve the claim of the failed request depends on the used command
Invoke-AzRestMethod
Invoke-AzRestMethodstores the claim challenge in the response body
# invoke some request that needs extra claim (FIDO2)
$response = Invoke-AzRestMethod -Uri $restUri -Method PUT -Payload ($requestBody | ConvertTo-Json -Depth 10)
# when PIM requires FIDO/Passkey or specific MFA, the API returns 400 with a claims challenge
$exception = ($response.Content | ConvertFrom-Json).error.message
if ($exception -and $exception -match 'claims=([^"]+)') {
$claimsChallenge = $matches[1]
$claimsChallenge = [System.Uri]::UnescapeDataString($claimsChallenge)
$claimsChallenge
}
Invoke-MgGraphRequest
Invoke-MgGraphRequeststores the claim challenge in the thrown error object
try {
$uri = "v1.0/roleManagement/directory/roleAssignmentScheduleRequests"
$response = Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body -ErrorAction Stop
} catch {
# When PIM requires FIDO/Passkey or specific MFA, the API returns 403 with a claims challenge.
$exception = $_.ErrorDetails.Message
if ($exception -and $exception -match 'claims=([^"]+)') {
$claimsChallenge = $matches[1]
$claimsChallenge = [System.Uri]::UnescapeDataString($claimsChallenge)
$claimsChallenge
}
}
How to translate the authentication context ID to DisplayName
In my examples, I use c1 as an authentication context ID.
In the Azure portal, the Authentication contexts menu doesnโt show the ID. But you can use the Developer Tools browser feature to get it. And as you can see, it belongs to the PIM_Elevation_PassKey context in my case.

You can, of course, use the Graph api beta/identity/conditionalAccess/authenticationContextClassReferences to retrieve authentication contexts as well.
Invoke-MgGraphRequest -Uri "beta/identity/conditionalAccess/authenticationContextClassReferences"

Real-world examples
Activating the Azure Resource PIM role (IAM)
- Invoke-PIMResourceRoleActivation from my AzurePIMStuff module.
Activating the Azure Directory PIM role
- Invoke-PIMDirectoryRoleActivation from my AzurePIMStuff module.




