PowerShell Setup: Control 1.23 — Step-Up Authentication for AI Agent Operations
Read the FSI PowerShell baseline first
Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, sovereign-cloud (GCC / GCC High / DoD) endpoints, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission. Snippets below show abbreviated patterns; the baseline is authoritative.
Last Updated: April 2026
Modules Required: Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Identity.Governance, Microsoft.Graph.Reports
Graph API surfaces used: /identity/conditionalAccess/authenticationContextClassReferences, /identity/conditionalAccess/policies, /policies/authenticationStrengthPolicies, /auditLogs/signIns
Prerequisites
# Pin module versions per the FSI PowerShell baseline (see _shared/powershell-baseline.md for current pins)
Install-Module Microsoft.Graph.Identity.SignIns -Scope CurrentUser -Force
Install-Module Microsoft.Graph.Identity.Governance -Scope CurrentUser -Force
Install-Module Microsoft.Graph.Reports -Scope CurrentUser -Force
Sovereign cloud note: For GCC High and DoD tenants, append
-Environment USGovor-Environment USGovDoDto everyConnect-MgGraphcall.
Script 1 — Create Authentication Contexts (c1–c5)
Authentication contexts can be created via Microsoft Graph (POST /identity/conditionalAccess/authenticationContextClassReferences). The cmdlet is New-MgIdentityConditionalAccessAuthenticationContextClassReference.
<#
.SYNOPSIS
Creates the five FSI step-up authentication contexts (c1-c5) in Entra.
.DESCRIPTION
Idempotent. Re-running updates the display name and description of an
existing context but never deletes one. Authentication context IDs are
immutable once created.
.EXAMPLE
.\New-FsiAuthenticationContexts.ps1 -WhatIf
.\New-FsiAuthenticationContexts.ps1
#>
[CmdletBinding(SupportsShouldProcess)]
param()
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess" -NoWelcome | Out-Null
$contexts = @(
@{ Id = 'c1'; DisplayName = 'Financial Transaction'; Description = 'Critical - 15 min fresh auth' }
@{ Id = 'c2'; DisplayName = 'Data Export'; Description = 'High - 30 min fresh auth' }
@{ Id = 'c3'; DisplayName = 'External API Call'; Description = 'High - 30 min fresh auth' }
@{ Id = 'c4'; DisplayName = 'Configuration Change'; Description = 'High - 30 min fresh auth' }
@{ Id = 'c5'; DisplayName = 'Sensitive Query'; Description = 'Medium - 60 min fresh auth' }
)
foreach ($ctx in $contexts) {
$existing = Get-MgIdentityConditionalAccessAuthenticationContextClassReference `
-AuthenticationContextClassReferenceId $ctx.Id -ErrorAction SilentlyContinue
if ($existing) {
if ($PSCmdlet.ShouldProcess($ctx.Id, 'Update authentication context')) {
Update-MgIdentityConditionalAccessAuthenticationContextClassReference `
-AuthenticationContextClassReferenceId $ctx.Id `
-DisplayName $ctx.DisplayName `
-Description $ctx.Description `
-IsAvailable:$true | Out-Null
Write-Host "[UPDATED] $($ctx.Id) - $($ctx.DisplayName)" -ForegroundColor Yellow
}
}
else {
if ($PSCmdlet.ShouldProcess($ctx.Id, 'Create authentication context')) {
New-MgIdentityConditionalAccessAuthenticationContextClassReference `
-Id $ctx.Id `
-DisplayName $ctx.DisplayName `
-Description $ctx.Description `
-IsAvailable:$true | Out-Null
Write-Host "[CREATED] $($ctx.Id) - $($ctx.DisplayName)" -ForegroundColor Green
}
}
}
Disconnect-MgGraph | Out-Null
Script 2 — Deploy Step-Up Conditional Access Policies (Report-Only)
<#
.SYNOPSIS
Creates the five FSI-StepUp-* CA policies in REPORT-ONLY state.
.DESCRIPTION
Use -BreakGlassGroupId to exclude the break-glass exclusion group.
Re-run is idempotent: existing policies with the same name are updated,
not duplicated. Policies are created in 'enabledForReportingButNotEnforced'
state per the FSI 72-hour bake requirement.
.PARAMETER BreakGlassGroupId
Object ID of the Entra group containing break-glass accounts.
.PARAMETER PhishingResistantStrengthId
Object ID of the Authentication Strength to require. Defaults to the
built-in 'Phishing-resistant MFA' strength.
.EXAMPLE
.\New-FsiStepUpPolicies.ps1 -BreakGlassGroupId '<guid>' -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)] [string] $BreakGlassGroupId,
[string] $PhishingResistantStrengthId = '00000000-0000-0000-0000-000000000004'
)
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess","Policy.Read.All" -NoWelcome | Out-Null
$matrix = @(
@{ Name='FSI-StepUp-c1-FinancialTxn'; Ctx='c1'; Freq=15 }
@{ Name='FSI-StepUp-c2-DataExport'; Ctx='c2'; Freq=30 }
@{ Name='FSI-StepUp-c3-ExternalAPI'; Ctx='c3'; Freq=30 }
@{ Name='FSI-StepUp-c4-ConfigChange'; Ctx='c4'; Freq=30 }
@{ Name='FSI-StepUp-c5-SensitiveQuery'; Ctx='c5'; Freq=60 }
)
foreach ($row in $matrix) {
$body = @{
displayName = $row.Name
state = 'enabledForReportingButNotEnforced' # Report-only
conditions = @{
users = @{
includeUsers = @('All')
excludeGroups = @($BreakGlassGroupId)
}
applications = @{
includeAuthenticationContextClassReferences = @($row.Ctx)
}
clientAppTypes = @('all')
}
grantControls = @{
operator = 'AND'
authenticationStrength = @{ id = $PhishingResistantStrengthId }
}
sessionControls = @{
signInFrequency = @{
value = $row.Freq
type = 'minutes'
authenticationType= 'primaryAndSecondaryAuthentication'
frequencyInterval = 'timeBased'
isEnabled = $true
}
persistentBrowser = @{ mode = 'never'; isEnabled = $true }
}
}
$existing = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$($row.Name)'" -ErrorAction SilentlyContinue
if ($existing) {
if ($PSCmdlet.ShouldProcess($row.Name, 'Update step-up CA policy')) {
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $existing.Id -BodyParameter $body | Out-Null
Write-Host "[UPDATED] $($row.Name)" -ForegroundColor Yellow
}
}
else {
if ($PSCmdlet.ShouldProcess($row.Name, 'Create step-up CA policy')) {
New-MgIdentityConditionalAccessPolicy -BodyParameter $body | Out-Null
Write-Host "[CREATED] $($row.Name) (report-only)" -ForegroundColor Green
}
}
}
Disconnect-MgGraph | Out-Null
Reminder: Promote each policy to
enabledonly after a 72-hour bake window with zero unintended Failure outcomes in Sign-in logs → Conditional Access → Report-only.
Script 3 — Validate Control 1.23 Configuration
<#
.SYNOPSIS
Validates Control 1.23 - returns a structured object with PASS/FAIL per check.
.OUTPUTS
PSCustomObject with: AuthContexts, StepUpPolicies, RiskPolicies, AuthStrengths,
BreakGlassExclusions, OverallStatus.
#>
[CmdletBinding()]
param(
[string[]] $RequiredContexts = @('c1','c2','c3','c4','c5'),
[string[]] $RequiredPolicies = @(
'FSI-StepUp-c1-FinancialTxn','FSI-StepUp-c2-DataExport',
'FSI-StepUp-c3-ExternalAPI','FSI-StepUp-c4-ConfigChange',
'FSI-StepUp-c5-SensitiveQuery'),
[Parameter(Mandatory)] [string] $BreakGlassGroupId
)
Connect-MgGraph -Scopes "Policy.Read.All" -NoWelcome | Out-Null
# Check 1: Authentication contexts exist and are published
$ctxResults = foreach ($id in $RequiredContexts) {
$c = Get-MgIdentityConditionalAccessAuthenticationContextClassReference `
-AuthenticationContextClassReferenceId $id -ErrorAction SilentlyContinue
[PSCustomObject]@{
Context = $id
Exists = [bool]$c
Published = $c.IsAvailable -eq $true
Status = if ($c -and $c.IsAvailable) { 'PASS' } else { 'FAIL' }
}
}
# Check 2: Step-up policies exist, are 'enabled', target the right context
$polResults = foreach ($name in $RequiredPolicies) {
$p = Get-MgIdentityConditionalAccessPolicy -Filter "displayName eq '$name'" -ErrorAction SilentlyContinue
$excludesBG = $p.Conditions.Users.ExcludeGroups -contains $BreakGlassGroupId
[PSCustomObject]@{
Policy = $name
Exists = [bool]$p
State = $p.State
ExcludesBreakGlass = $excludesBG
Status = if ($p -and $p.State -eq 'enabled' -and $excludesBG) { 'PASS' } else { 'FAIL' }
}
}
# Check 3: Risk-based policies are present
$riskPolicies = Get-MgIdentityConditionalAccessPolicy | Where-Object {
$_.Conditions.SignInRiskLevels.Count -gt 0 -or $_.Conditions.UserRiskLevels.Count -gt 0
}
# Check 4: Phishing-resistant strength is in use somewhere
$strengths = Get-MgPolicyAuthenticationStrengthPolicy
$result = [PSCustomObject]@{
AuthContexts = $ctxResults
StepUpPolicies = $polResults
RiskPoliciesCount = $riskPolicies.Count
AuthStrengthsCount = $strengths.Count
OverallStatus = if (
($ctxResults.Status -notcontains 'FAIL') -and
($polResults.Status -notcontains 'FAIL') -and
($riskPolicies.Count -ge 1)
) { 'PASS' } else { 'FAIL' }
}
Disconnect-MgGraph | Out-Null
$result
Script 4 — Export Step-Up Evidence (SHA-256 Hashed)
<#
.SYNOPSIS
Exports CA policy snapshots and recent step-up sign-in events for FSI examination
evidence. Writes a manifest with SHA-256 hashes per the FSI baseline.
.PARAMETER OutputPath
Output directory. Defaults to .\evidence\1.23\<UTC-timestamp>\.
.PARAMETER LookbackDays
Sign-in log lookback window. Defaults to 7. Maximum 30 days for the Graph API.
#>
[CmdletBinding()]
param(
[string] $OutputPath = ".\evidence\1.23\$(Get-Date -Format 'yyyyMMddTHHmmssZ' -AsUTC)",
[int] $LookbackDays = 7
)
New-Item -ItemType Directory -Force -Path $OutputPath | Out-Null
Connect-MgGraph -Scopes "Policy.Read.All","AuditLog.Read.All" -NoWelcome | Out-Null
# Snapshot all step-up CA policies
$polFile = Join-Path $OutputPath 'ca-policies-stepup.json'
Get-MgIdentityConditionalAccessPolicy |
Where-Object {
$_.DisplayName -like 'FSI-StepUp-*' -or
$_.Conditions.Applications.IncludeAuthenticationContextClassReferences.Count -gt 0
} |
ConvertTo-Json -Depth 10 | Set-Content -Path $polFile -Encoding UTF8
# Snapshot authentication contexts
$ctxFile = Join-Path $OutputPath 'authentication-contexts.json'
Get-MgIdentityConditionalAccessAuthenticationContextClassReference |
ConvertTo-Json -Depth 5 | Set-Content -Path $ctxFile -Encoding UTF8
# Snapshot authentication strengths
$strFile = Join-Path $OutputPath 'authentication-strengths.json'
Get-MgPolicyAuthenticationStrengthPolicy |
ConvertTo-Json -Depth 10 | Set-Content -Path $strFile -Encoding UTF8
# Sign-in events that requested an authentication context
$since = (Get-Date).AddDays(-$LookbackDays).ToString('yyyy-MM-ddTHH:mm:ssZ')
$signInFile = Join-Path $OutputPath 'signins-with-authcontext.json'
$signIns = Get-MgAuditLogSignIn -Filter "createdDateTime ge $since" -All |
Where-Object { $_.AuthenticationContextClassReferences.Count -gt 0 }
$signIns | ConvertTo-Json -Depth 10 | Set-Content -Path $signInFile -Encoding UTF8
# Manifest with SHA-256 per FSI baseline
$manifest = Get-ChildItem $OutputPath -File | ForEach-Object {
[PSCustomObject]@{
File = $_.Name
SizeBytes= $_.Length
SHA256 = (Get-FileHash -Algorithm SHA256 -Path $_.FullName).Hash
}
}
$manifest | ConvertTo-Json -Depth 3 |
Set-Content -Path (Join-Path $OutputPath 'manifest.json') -Encoding UTF8
Disconnect-MgGraph | Out-Null
Write-Host "Evidence written to $OutputPath" -ForegroundColor Green
Sentinel / Defender XDR KQL — Step-Up Failure Detection
Save as analytics rules per Step 7 of the portal walkthrough.
// Step-up failure spike: >5 failures in 10 minutes for the same user
SigninLogs
| where TimeGenerated > ago(15m)
| where AuthenticationContextClassReferences contains "c"
| where ResultType != 0
| summarize FailureCount = count() by UserPrincipalName, bin(TimeGenerated, 10m)
| where FailureCount > 5
// Conditional Access policy state change on FSI-StepUp-* policies
AuditLogs
| where ActivityDisplayName in ("Update conditional access policy","Delete conditional access policy")
| where TargetResources has "FSI-StepUp-"
| project TimeGenerated, InitiatedBy, ActivityDisplayName, Result, TargetResources
// PIM activation where required authentication context was not satisfied
AuditLogs
| where Category == "RoleManagement"
| where ActivityDisplayName == "Add member to role completed (PIM activation)"
| extend ContextRequested = tostring(parse_json(AdditionalDetails)[0].value)
| where isnotempty(ContextRequested)
| join kind=leftouter (
SigninLogs
| where AuthenticationContextClassReferences contains "c"
| project UserId, SigninAuthContext = AuthenticationContextClassReferences, SignInTime = TimeGenerated
) on $left.InitiatedBy.user.id == $right.UserId
| where SigninAuthContext !contains ContextRequested or isnull(SigninAuthContext)
Back to Control 1.23 | Portal Walkthrough | Verification Testing | Troubleshooting