# 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 🙂.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765449952157/eec35ec8-ba8a-42e4-b3c3-2de887ed8e2c.png align="center")

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](https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview?toc=%2Fentra%2Fidentity%2Fprivileged-identity-management%2Ftoc.json&bc=%2Fentra%2Fidentity%2Fid-governance%2Fprivileged-identity-management%2Fbreadcrumb%2Ftoc.json#mapping-of-rule-ids-to-pim-role-settings-on-the-microsoft-entra-admin-center)).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765453729310/d164746b-e70d-4037-98ae-25c4f1429a21.png align="center")

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
    

```powershell
# 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
    

```powershell
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**](https://www.powershellgallery.com/packages/AzurePIMStuff) module.

```powershell
$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

```powershell
#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
    

```powershell
# 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
    

```powershell
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765451147245/ad6c0381-c549-4788-addb-c06c1cc7dfc7.png align="center")

---

You can, of course, use the Graph api `beta/identity/conditionalAccess/authenticationContextClassReferences` to retrieve authentication contexts as well.

```powershell
Invoke-MgGraphRequest -Uri "beta/identity/conditionalAccess/authenticationContextClassReferences"
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765452596056/fee34d09-e0bd-413f-9dd8-ef8ffc0f0a59.png align="center")

---

# Real-world examples

**Activating the Azure Resource PIM role (IAM)**

* [Invoke-PIMResourceRoleActivation](https://github.com/ztrhgf/useful_powershell_modules/blob/main/AzurePIMStuff_source/Invoke-PIMResourceRoleActivation.ps1) from my [**AzurePIMStuff**](https://www.powershellgallery.com/packages/AzurePIMStuff) module.
    

**Activating the Azure Directory PIM role**

* [Invoke-PIMDirectoryRoleActivation](https://github.com/ztrhgf/useful_powershell_modules/blob/main/AzurePIMStuff_source/Invoke-PIMDirectoryRoleActivation.ps1) from my [**AzurePIMStuff**](https://www.powershellgallery.com/packages/AzurePIMStuff) module.
