Skip to main content

Command Palette

Search for a command to run...

Automating Azure PIM role activation secured with FIDO2/Passkey with PowerShell

Or any other Azure service requiring special authentication strength

Updated
โ€ข6 min read
Automating Azure PIM role activation secured with FIDO2/Passkey with PowerShell

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-AzRestMethod response body

  • Unescape the HTML string and convert it to base64

  • Use such a claim in the Claims parameter of the Connect-AzAccount to 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-AzAccount to retrieve the access token

  • Instead, we will need:

    • A list of required Graph Api scopes ($scope)

      • (RoleAssignmentSchedule.ReadWrite.Directory to 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"}}}'

        • c1 is 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-AzRestMethod stores 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-MgGraphRequest stores 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)

Activating the Azure Directory PIM role