Skip to content

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 USGov or -Environment USGovDoD to every Connect-MgGraph call.


Script 1 — Create Authentication Contexts (c1c5)

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 enabled only 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