Activating PIM Roles that require MFA or Conditional Access Authentication Context with PowerShell

For some time, I’ve been activating and scheduling activations for Azure roles under Privileged Identity Management (PIM) using the Microsoft Graph PowerShell SDK. However recently we secured these role activations to require a conditional access authentication context with MFA. This basically requires me to MFA when I activate a role with PIM.

Problem is, by default the PowerShell SDK does not support an authentication flow with this additional requirement, and therefore you get a nice RoleAssignmentRequestAcrsValidationFailed error:

This is because the access token doesn’t satisfy the requirements of the conditional access policy. To get around this problem, we need to get our own access token for Microsoft Graph and add an additional claim that will satisfy the requirements of the policy. We can then pass this enhanced token to the Connect-MgGraph cmdlet and happily activate our secured role assignments.

You can read more about how this works in the following MS document: https://learn.microsoft.com/en-us/entra/identity-platform/claims-challenge?tabs=dotnet

First, we need to know the authorization context claim value of the conditional access policy being used by the role activation. To find this, run the following code using the PowerShell SDK:

# Retrieve all Conditional Access policies that use authentication context from Microsoft Graph
Connect-MgGraph
$policies = Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.Conditions.Applications.IncludeAuthenticationContextClassReferences -ne $null }
$policies | ForEach-Object {
    [PSCustomObject]@{
        DisplayName = $_.DisplayName
        State       = $_.State
        AuthContext = $_.Conditions.Applications.IncludeAuthenticationContextClassReferences
    }
} | Format-Table -AutoSize

From the results, find your policy and note the AuthContext value:

Then set some parameters for the code that will follow:

# Define your application details
$clientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e" # this is the app Id used by the PowerShell SDK
$tenantId = "<Your tenant Id here>"
$redirectUri = "http://localhost:8080"
$scope = "https://graph.microsoft.com/.default"
$claimValue = "c1" # The value of the authentication context claim from the conditional access policy

Next, run the following code to get an authorization code to use when requesting an access token. Note that we are requesting the “acrs” claim, which is the auth context Id. Since my conditional access policy requires MFA, running this code will create an MFA challenge before returning the auth code.

# Encode the additional claims
$additionalClaims = [ordered]@{"access_token" = [ordered]@{"acrs" = [ordered]@{"essential" = $true; "value" = $claimValue}}}
$encodedClaims = [System.Web.HttpUtility]::UrlEncode(($additionalClaims | ConvertTo-Json -Compress))

# Generate the authorization URL
$authUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectUri&response_mode=query&scope=$scope&claims=$encodedClaims"

# Start a local HTTP listener to capture the authorization code
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add($redirectUri + "/")
$listener.Start()

# Open the authorization URL in the default browser
Start-Process $authUrl

# Wait for the authorization response
Write-Host "Waiting for authorization response..."
$context = $listener.GetContext()
$response = $context.Response
$request = $context.Request

# Extract the authorization code from the query parameters
$authCode = $request.QueryString["code"]

if ($authCode) 
{
    $responseString = "Authorization code received. You can close this window."
} 
else 
{
    $responseString = "No authorization code received. Please try again."
}

# Send a response to the browser
$buffer = [System.Text.Encoding]::UTF8.GetBytes($responseString)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()

# Stop the listener
$listener.Stop()
$listener.Dispose()

If the auth code was successfully received, you can close the browser window. The $authcode variable will contain the required auth code.

We can then go ahead and request an access token using this code, and in the request we will add the “xms_cc” claim. This is the “client capabilities” claim that indicates it can satisfy the conditional access requirements.

# Get Access Token
$Body = @{
    grant_type   = 'authorization_code'
    client_id    = $ClientID  
    scope        = $Scope 
    code         = $AuthCode  
    redirect_uri = $RedirectUri 
    claims        = '{"access_token":{"xms_cc":{"values":["cp1"]}}}' # Request the xms_cc optional claim with the value cp1
}
$Response = Invoke-RestMethod "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Method POST -Body $Body
$accessToken = $Response.access_token

Now that we have our access token with the required claims, we can pass this to Connect-MgGraph:

# Connect to Microsoft Graph with the access token
$graphToken = $accessToken | ConvertTo-SecureString -AsPlainText -Force
$null = Disconnect-MgGraph -ErrorAction SilentlyContinue
Connect-MgGraph -NoWelcome -AccessToken $graphToken

Then finally we can activate a PIM role with the usual code. This will work since the access token used to connect to MS Graph now satisfies the requirements of the conditional access policy.

# Activate a PIM role assignment
$subjectId = "6981d298-1234-5678-9abc-9c7ddee0a65b" # The Entra object Id of the user to activate the role assignment for
$roleDefinitionId = "2b745bdf-0803-4d80-aa65-822c4493daac" # The role definition Id of the role assignment to activate ("Office Apps Administrator")
$assignmentDuration = "PT1H" # The duration of the role assignment in ISO 8601 format (1 hour)
$StartDateTime = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
$params = @{
    "PrincipalId" = $SubjectID
    "RoleDefinitionId" = $roleDefinitionId
    "Justification" = "Activate assignment"
    "DirectoryScopeId" = "/"
    "Action" = "SelfActivate"
    "ScheduleInfo" = @{
        "StartDateTime" = $StartDateTime
        "Expiration" = @{
            "Type" = "AfterDuration"
            "Duration" = $AssignmentDuration
            }
    }
}
    
New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params 

This process is using the Entra authorization flows directly, which Microsoft actually prefer you don’t do – they would like you to use their supported code libraries for this. I do that myself – I have code that uses the Microsoft.Identity.Client library to get an access token, but that does have additional module and library requirements. Using these auth flows directly is gonna get you the same end result 🙂

You can find the full code example for the above here:

# Use this code to retrieve all Conditional Access policies that use authentication context from Microsoft Graph
Connect-MgGraph
$policies = Get-MgIdentityConditionalAccessPolicy | Where-Object { $_.Conditions.Applications.IncludeAuthenticationContextClassReferences -ne $null }
$policies | ForEach-Object {
[PSCustomObject]@{
DisplayName = $_.DisplayName
State = $_.State
AuthContext = $_.Conditions.Applications.IncludeAuthenticationContextClassReferences
}
} | Format-Table -AutoSize
# Define your application details
$clientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e" # The app Id used by the Graph SDK
$tenantId = "<Your tenant Id here>"
$redirectUri = "http://localhost:8080&quot;
$scope = "https://graph.microsoft.com/.default&quot;
$claimValue = "c1" # The value of the authentication context claim from the conditional access policy
# Encode the additional claims
$additionalClaims = [ordered]@{"access_token" = [ordered]@{"acrs" = [ordered]@{"essential" = $true; "value" = $claimValue}}}
$encodedClaims = [System.Web.HttpUtility]::UrlEncode(($additionalClaims | ConvertTo-Json -Compress))
# Generate the authorization URL
$authUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectUri&response_mode=query&scope=$scope&claims=$encodedClaims&quot;
# Start a local HTTP listener to capture the authorization code
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add($redirectUri + "/")
$listener.Start()
# Open the authorization URL in the default browser
Start-Process $authUrl
# Wait for the authorization response
Write-Host "Waiting for authorization response…"
$context = $listener.GetContext()
$response = $context.Response
$request = $context.Request
# Extract the authorization code from the query parameters
$authCode = $request.QueryString["code"]
if ($authCode)
{
$responseString = "Authorization code received. You can close this window."
}
else
{
$responseString = "No authorization code received. Please try again."
}
# Send a response to the browser
$buffer = [System.Text.Encoding]::UTF8.GetBytes($responseString)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
# Stop the listener
$listener.Stop()
$listener.Dispose()
# Get Access Token
$Body = @{
grant_type = 'authorization_code'
client_id = $ClientID
scope = $Scope
code = $AuthCode
redirect_uri = $RedirectUri
claims = '{"access_token":{"xms_cc":{"values":["cp1"]}}}' # Request the xms_cc optional claim with the value cp1
}
$Response = Invoke-RestMethod "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token&quot; -Method POST -Body $Body
$accessToken = $Response.access_token
# Connect to Microsoft Graph with the access token
$graphToken = $accessToken | ConvertTo-SecureString -AsPlainText -Force
$null = Disconnect-MgGraph -ErrorAction SilentlyContinue
Connect-MgGraph -NoWelcome -AccessToken $graphToken
# Activate a PIM role assignment
$subjectId = "6981d298-1234-5678-9abc-9c7ddee0a65b" # The Entra object Id of the user to activate the role assignment for
$roleDefinitionId = "2b745bdf-0803-4d80-aa65-822c4493daac" # The role definition Id of the role assignment to activate (eg "Office Apps Administrator")
$assignmentDuration = "PT1H" # The duration of the role assignment in ISO 8601 format (1 hour)
$StartDateTime = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
$params = @{
"PrincipalId" = $SubjectID
"RoleDefinitionId" = $roleDefinitionId
"Justification" = "Activate assignment"
"DirectoryScopeId" = "/"
"Action" = "SelfActivate"
"ScheduleInfo" = @{
"StartDateTime" = $StartDateTime
"Expiration" = @{
"Type" = "AfterDuration"
"Duration" = $AssignmentDuration
}
}
}
$response = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params