次の方法で共有


必須の MFA 要件のロールアウト後にユーザーがサインインできないテナントの強制を延期する方法

MFA を使用するための必須要件がテナントにロールアウトされた後に MFA メソッドの使用に問題がある場合、ユーザーは Azure portal、Microsoft Entra 管理センター、または Microsoft Intune 管理センターにサインインできない可能性があります。

ユーザーがサインインできない場合は、グローバル管理者として次のスクリプトを実行して、テナントの MFA 適用を一時的に延期できます。

Azure の必須 MFA 要件の詳細については、「 Azure およびその他の管理ポータルの必須多要素認証の計画」を参照してください。 次のスクリプトは、フェーズ 1 のアプリケーションにのみ適用されます。

スクリプト アクション

このスクリプトは、次のアクションを実行します。

  • ユーザーのテナントがある場合はそれを選択するか、ユーザーが選択できるテナントの一覧を表示します。 必要に応じて、スクリプトは施行日を要求します。 デフォルトの日付は 2025 年 9 月 30 日です。
  • ユーザーをそのテナントにログインします。
  • 関連する認証トークンを取得します。
  • ユーザーが昇格されたアクセス権を持っているかどうかを確認します。 そうでない場合は、スクリプトが昇格を行います。
  • 設定リソース プロバイダー (RP) でユーザーに適切なロールが割り当てられているかどうかを確認します。 そうでない場合は、スクリプトによって適切なロールが割り当てられます。
  • Entra ID の施行日を更新します。
  • スクリプトによって追加された昇格されたアクセスを削除しようとします。

[前提条件]

スクリプト

param (
    [Parameter(Mandatory=$false)]
    [string]$TenantId,

    [Parameter(Mandatory=$false)]
    [string]$PostponementDateInUTC
)

# Make sure the Az.Accounts module is imported
Import-Module Az.Accounts

function Set-TenantSettingsMFAPostponement {

    Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope Process

    $cutoffDate = [datetime]::Parse("2025-10-01T00:00:00Z")
    $isDefaultDate = $false
    if ($PostponementDateInUTC) {
        # ISO 8601 check (basic): YYYY-MM-DDTHH:mm:ssZ
        if ($PostponementDateInUTC -notmatch '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$') {
            Write-Host "Invalid PostponementDateInUTC format. Must be in ISO 8601 format like '2025-09-30T23:59:59Z'." -ForegroundColor Red
            return
        }
        $valid = $false
        $today = [DateTime]::UtcNow.Date
        $minDate = $today.AddDays(1)
        $maxDate = [DateTime]::ParseExact("2025-09-30T23:59:59Z", "yyyy-MM-ddTHH:mm:ssZ", $null).ToUniversalTime()
        $valid = Check-Date-Is-Valid -maxDate $maxDate -minDate $minDate -dateToCheck $PostponementDateInUTC
        if (-not $valid) {
            return
        }
    } else {
        $PostponementDateInUTC = "2025-09-30T23:59:59Z"
        $isDefaultDate = $true
    }

    # If user didn't specify a tenant in params, let them select.
    if (-not $TenantId) {
        try {
            # Have user log into relevant account
            $connected = Connect-AzAccount -ErrorAction Stop
            # Get all tenants the user has access to
            $tenants = Get-AzTenant -ErrorAction Stop
        } catch {
            Write-Host "Failed to connect and/or fetch list of user's tenants. Error: $($_.Exception.Message)" -ForegroundColor Red
            Write-Host
            return
        }

        if (-not $tenants) {
            Write-Host "No tenants found for this user." -ForegroundColor Red
            return
        }

        # Display them as a numbered list
        Write-Host "Please select a tenant from the list below"
        Write-Host " "
        for ($i = 0; $i -lt $tenants.Count; $i++) {
            Write-Host "$($i + 1)) $($tenants[$i].TenantId) - $($tenants[$i].Name) ($($tenants[$i].DefaultDomain))"
        }
        Write-Host

        # Ask user to select one
        $selection = Read-Host "Enter the number for the tenant you want to use"

        # Validate and extract selected tenant
        if ($selection -match '^\d+$') {
            $selection = [int]$selection
            if ($selection -ge 1 -and $selection -le $tenants.Count) {
                $chosenTenant = $tenants[$selection - 1]
                Write-Host "You selected tenant: $($chosenTenant.TenantId) - $($chosenTenant.Name) ($($chosenTenant.DefaultDomain))" -ForegroundColor Green
                Write-Host
                # Use $chosenTenant.TenantId later in the script
                $TenantId = $chosenTenant.TenantId
            } else {
                Write-Host "Number is out of range. Exiting..." -ForegroundColor Red
                return
            }
        } else {
            Write-Host "Invalid selection. Exiting..." -ForegroundColor Red
            return
        }
    }

    if ($isDefaultDate) {
        $newDate = Select-Postponement-Date
        if (-not $newDate) {
            return
        } else {
            $PostponementDateInUTC = $newDate.ToString("yyyy-MM-ddTHH:mm:ssZ")
            $isDefaultDate = $false
        }
    }

    if ($isDefaultDate) {
        Write-Host "This will update the MFA enforcement date for TenantId: '$($TenantId)' to the DEFAULT date of '$($PostponementDateInUTC)'"
    } else {
        Write-Host "This will update the MFA enforcement date for TenantId: '$($TenantId)' to the date of '$($PostponementDateInUTC)'"
    }
    
    Write-Host
    $confirmation = Read-Host "Do you want to continue (Y/N)?"
    if ($confirmation -match '^[Yy]$') {
        Write-Host "Proceeding..." -ForegroundColor Green
        Write-Host
    } else {
        Write-Host "Operation canceled by user." -ForegroundColor Red
        return
    }

    try {
        $connected = Connect-AzAccount -TenantId $TenantId
    } catch {
        Write-Host "Failed to log the user in to specified tenant. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }
    Start-Sleep -Seconds 3

    # Constants
    $ELEVATED_TENANT_ADMIN_ROLE_ID = "/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9"
    $OWNER_ROLE_ID = "/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635"

    # Get tokens
    Write-Host "Fetching necessary authorization tokens..."
    try {
        $armToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/").Token
        if ($null -eq $armToken) {
            Write-Host "Failed to fetch an authorization token for Azure Resource Manager. Make sure you run: Connect-AzAccount -TenantId '<your tenant id>'" -ForegroundColor Red
            return
        }
            
        Start-Sleep -Seconds 3
    } catch {
        Write-Host "Failed to fetch Azure Resource Manager token. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }

    try {
        $coreToken = (Get-AzAccessToken -ResourceUrl "https://management.core.windows.net/").Token
        if ($null -eq $coreToken) {
            Write-Host "Failed to fetch an authorization token for Azure Resource Manager core. Make sure you run: Connect-AzAccount -TenantId '<your tenant id>'" -ForegroundColor Red
            return
        }
            
        Start-Sleep -Seconds 3
    } catch {
        Write-Host "Failed to fetch Azure Resource Manager token. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }
    
    $armClaims = Decode-JwtPayload -Jwt $armToken
    $objectId = $armClaims.oid
    if ($null -eq $objectId) {
        Write-Host "Failed to parse objectId from oid claim in Azure Resource Manager token. Make sure you are an admin of this tenant." -ForegroundColor Red
        return
    }
    
    Write-Host "Successfully fetched authorization tokens." -ForegroundColor Green
    Write-Host

    # Check elevated access
    try {
        $roleCheckUri = "https://management.azure.com/providers/Microsoft.PortalServices/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId eq '$($objectId)'"
        $roleAssignments = Invoke-RestMethod -Headers @{Authorization = "Bearer $armToken"} -Uri $roleCheckUri -Method GET
            
        Start-Sleep -Seconds 3
    } catch {
        Write-Host "Failed to check user's elevated access. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }

    Write-Host "Checking for elevated access..."
    $hasElevatedAccess = $false
    foreach ($item in $roleAssignments.value) {
        if ($item.properties.roleDefinitionId -eq $ELEVATED_TENANT_ADMIN_ROLE_ID) {
            $hasElevatedAccess = $true
        }
    }

    # Used to determine whether or not to delete elevated access at end
    $alreadyHadElevatedStatus = $hasElevatedAccess
    
    if (-not $hasElevatedAccess) {
        Write-Host "User does NOT have elevated access. Elevating access..."
        $elevateUri = "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2017-05-01"

        try {
            # Attempt the API call and capture the response
            $response = Invoke-RestMethod -Headers @{Authorization = "Bearer $coreToken"} -Uri $elevateUri -Method POST -ErrorAction Stop

            # Even if there's no content in the response, the request could still have succeeded
            Write-Host "Successfully elevated access." -ForegroundColor Green
            Write-Host
            
            Start-Sleep -Seconds 3
        } catch {
            Write-Host "Failed to elevate access. Error: $($_.Exception.Message)"
            Write-Host "Make sure you are already a tenant admin"
            return
        }
    } else {
        Write-Host "User already has elevated access." -ForegroundColor Green
        Write-Host
    }

    try {
        # Re-check role assignments after possible elevation
        $roleAssignments = Invoke-RestMethod -Headers @{Authorization = "Bearer $armToken"} -Uri $roleCheckUri -Method GET
            
        Start-Sleep -Seconds 3
    } catch {
        Write-Host "Failed to re-check user's elevated access. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host

        # Clean up elevated access if we added it
        if (-not $alreadyHadElevatedStatus) {
            Remove-ElevatedAccess -objectId $objectId -TenantId $TenantId
        }
        return
    }

    Write-Host "Checking if owner role exists..."
    $hasOwnerRole = $false
    foreach ($item in $roleAssignments.value) {
        if ($item.properties.roleDefinitionId -eq $OWNER_ROLE_ID) {
            $hasOwnerRole = $true
        }
    }

    try {
        if (-not $hasOwnerRole) {
            Write-Host "Owner role does NOT exist. Assigning Owner Role..."
            
            # register provider
            $regProviderUri = "https://management.azure.com/providers/Microsoft.PortalServices/register?api-version=2024-03-01"
            try { 
                $providerRegistered = Invoke-RestMethod -Headers @{Authorization = "Bearer $armToken"} -Uri $regProviderUri -Method POST
                
                Start-Sleep -Seconds 3
            } catch {
                Write-Host "Failed to register PortalServices provider. Error: $($_.Exception.Message)" -ForegroundColor Red
                Write-Host
                throw "Provider registration failed"
            }

            # assign owner role
            $assignmentId = [guid]::NewGuid()
            $assignUri = "https://management.azure.com/providers/Microsoft.PortalServices/providers/Microsoft.Authorization/roleAssignments/$($assignmentId)?api-version=2020-04-01-preview"
            $assignBody = @{
                properties = @{
                    roleDefinitionId = $OWNER_ROLE_ID
                    principalId = $objectId
                    principalType = "User"
                    scope = "/providers/Microsoft.PortalServices"
                }
            } | ConvertTo-Json -Depth 5
            try {
                Invoke-RestMethod -Headers @{Authorization = "Bearer $armToken"; "Content-Type" = "application/json"} `
                    -Uri $assignUri -Method PUT -Body $assignBody
                Start-Sleep -Seconds 3
                Write-Host "Successfully assigned owner role." -ForegroundColor Green
            } catch {
                Write-Host "Failed to assign owner role. Error: $($_.Exception.Message)" -ForegroundColor Red
                Write-Host
                throw "Owner role assignment failed"
            }
        } else {
            Write-Host "Owner role already exists."
            Write-Host
        }

        # Update Tenant Settings
        Write-Host "Trying to postpone MFA enforcement..."
        $settingsUri = "https://management.azure.com/providers/Microsoft.PortalServices/settings/default?api-version=2024-09-01-preview"
        $settingsBody = @{
            properties = @{
                multiFactorAuthentication = @{
                    portalEnforcement = "OptOut"
                    portalJustification = "Postponed MFA by user with Powershell script"
                    portalEnforcementDate = $PostponementDateInUTC
                }
            }
        } | ConvertTo-Json -Depth 5

        $successfulUpdate = $false
        try {
            $updateResults = Invoke-WebRequest -Headers @{Authorization = "Bearer $armToken"; "Content-Type" = "application/json"} `
                -Uri $settingsUri -Method PUT -Body $settingsBody
                
            Start-Sleep -Seconds 3
        } catch {
            Write-Host "Failed to postpone MFA. Error: $($_.Exception.Message)" -ForegroundColor Red
            Write-Host
            throw "MFA postponement failed"
        }
        
        if ($updateResults.StatusCode -ge 200 -and $updateResults.StatusCode -lt 300) {
            # Convert content to JSON
            $jsonResponse = $updateResults.Content | ConvertFrom-Json

            # Check if provisioningState is 'Succeeded'
            if ($jsonResponse.properties.provisioningState -eq "Succeeded") {
                Write-Host "Successfully postponed MFA to $($PostponementDateInUTC)." -ForegroundColor Green
                Write-Host
                $successfulUpdate = $true
            } else {
                Write-Host "Provisioning state is not Succeeded. It is $($jsonResponse.properties.provisioningState)." -ForegroundColor Red
                Write-Host
                throw "MFA postponement failed - incorrect provisioning state"
            }
        } else {
            Write-Host "Request failed with status: $($updateResults.StatusCode)" -ForegroundColor Red
            Write-Host
            throw "MFA postponement failed - incorrect status code"
        }

        # Optional verification
        if ($successfulUpdate) {
            Write-Host "Verifying that postponement date was properly stored..."
            try {
                $verify = Invoke-RestMethod -Headers @{Authorization = "Bearer $armToken"} `
                    -Uri $settingsUri -Method GET
                
                Start-Sleep -Seconds 3

                Write-Host "The postponement date of '$($verify.properties.multiFactorAuthentication.portalEnforcementDate)' is set for tenant '$($TenantId)'" -ForegroundColor Green
                Write-Host
            } catch {
                Write-Host "Failed to fetch the stored postponement date. Error: $($_.Exception.Message)" -ForegroundColor Red
                Write-Host
                # Continue despite verification failure as update was successful
            }
        }
    }
    catch {
        Write-Host "An error occurred during the operation: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
    }
    finally {
        # Remove elevated access only if we were the ones that added it in the script.
        if (-not $alreadyHadElevatedStatus) {
            Remove-ElevatedAccess -objectId $objectId -TenantId $TenantId
        }
    }
}

function Remove-ElevatedAccess {
    param (
        [string]$objectId,
        [string]$TenantId
    )

    Write-Host "Removing temporary elevated access..."
    $roleCheckUri = "https://management.azure.com/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&`$filter=principalId+eq+'$($objectId)'"
    try {
        $roleAssignments = Invoke-RestMethod -Headers @{Authorization = "Bearer $armToken"} -Uri $roleCheckUri -Method GET
    } catch {
        Write-Host "Failed to fetch elevated access status. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }

    $newAssignmentId = $false
    foreach ($item in $roleAssignments.value) {
        if ($item.properties.roleDefinitionId -eq $ELEVATED_TENANT_ADMIN_ROLE_ID) {
            $newAssignmentId = $($item.name)
        }
    }

    if ($newAssignmentId -eq $false) {
        Write-Host "Could not find the elevated role assignment id. You will need to manually delete your elevated status.2" -ForegroundColor Red
        return
    }

    try {
        $connected = Connect-AzAccount -TenantId $TenantId
    } catch {
        Write-Host "Failed re-connect user. You will need to manually delete your elevated status. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }

    Write-Host "Refreshing authorization tokens..."
    Start-Sleep -Seconds 3
    try {
        $coreToken = (Get-AzAccessToken -ResourceUrl "https://management.core.windows.net/").Token
        if ($null -eq $coreToken) {
            Write-Host "Failed to fetch an authorization token for Azure Resource Manager core. You will need to manually delete your elevated status." -ForegroundColor Red
            return
        }
    } catch {
        Write-Host "Failed to refresh authorization tokens. You will need to manually delete your elevated status. Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host
        return
    }

    $retryCount = 0
    $maxRetries = 3

    do {
        $result = Delete-Elevated-Access -roleAssignmentId $newAssignmentId -coreToken $coreToken -retryCount $retryCount
        if ($result -eq $false) {
            $retryCount = $retryCount + 1
        } else {
            return
        }
    } while ($retryCount -lt $maxRetries)

    if ($retryCount -ge $maxRetries) {
        Write-Host "Failed to remove elevated access. You will need to manually delete your elevated status. " -ForegroundColor Red
        Write-Host
        return
    }
}

function Delete-Elevated-Access {
    param (
        [string]$roleAssignmentId,
        [string]$coreToken,
        [int]$retryCount
    )

    try {
        $deleteUri = "https://management.azure.com/providers/Microsoft.Authorization/roleAssignments/" + $roleAssignmentId + "?api-version=2018-07-01"
        # Attempt the API call and capture the response
        $response = Invoke-RestMethod -Headers @{Authorization = "Bearer $coreToken"} -Uri $deleteUri -Method DELETE -ErrorAction Stop

        # Even if there's no content in the response, the request could still have succeeded
        Write-Host "Successfully removed elevated access." -ForegroundColor Green
        Write-Host
        
        Start-Sleep -Seconds 3
        return $true
    } catch {
        Write-Host "(Attempt #$($retryCount)): Failed to remove elevated access. Error: $($_.Exception.Message)" -ForegroundColor Yellow
        Start-Sleep -Seconds 3
        return $false
    }
}

function Decode-JwtPayload {
    param (
        [string]$Jwt
    )

    $parts = $Jwt -split '\.'
    if ($parts.Count -lt 2) {
        throw "Invalid JWT format"
    }

    $payload = $parts[1]

    # Replace URL-safe base64 chars
    $payload = $payload.Replace('-', '+').Replace('_', '/')

    # Add padding if needed
    switch ($payload.Length % 4) {
        2 { $payload += '==' }
        3 { $payload += '=' }
        1 { throw "Invalid base64url string" }
    }

    $json = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload))
    return $json | ConvertFrom-Json
}

function Check-Date-Is-Valid {
    param (
        [DateTime]$maxDate,
        [DateTime]$minDate,
        [string]$dateToCheck
    )

    $inputDate = $maxDate
    if ([string]::IsNullOrWhiteSpace($dateToCheck)) {
        Write-Host "No input provided. Please enter a date in the required format.`n" -ForegroundColor Red
        return $valid
    }

    $parsed = [DateTime]::TryParse($dateToCheck, [ref]$inputDate)
    if (-not $parsed) {
        Write-Host "Invalid date format. Please try again using format like 2025-09-15T00:00:00Z.`n" -ForegroundColor Red
        return $valid
    }

    $inputDate = $inputDate.ToUniversalTime()
    if ($inputDate -ge $minDate -and $inputDate -le $maxDate) {
        return $inputDate
    } else {
        Write-Host "Date must be between $($minDate.ToString("u")) and $($maxDate.ToString("u")) (UTC). Try again.`n" -ForegroundColor Red
    }

    return $valid
}

function Select-Postponement-Date {
    $valid = $false
    $today = [DateTime]::UtcNow.Date
    $minDate = $today.AddDays(1)
    $maxDate = [DateTime]::ParseExact("2025-09-30T23:59:59Z", "yyyy-MM-ddTHH:mm:ssZ", $null).ToUniversalTime()

    $inputDate = $maxDate
    while (-not $valid) {
        $inputDateStr = Read-Host "Enter a UTC date up to 2025-09-30T23:59:59Z (e.g., 2025-09-15T00:00:00Z) or Enter to use the default" 
        $defaultChosen = $false
        if([string]::IsNullOrWhiteSpace($inputDateStr)) {
            $inputDateStr = "2025-09-30T23:59:59Z"
            $defaultChosen = $true
        }
        
        $inputDate = Check-Date-Is-Valid -maxDate $maxDate -minDate $minDate -dateToCheck $inputDateStr

        if (-not $inputDate) {
            $valid = $false
        } else {
            $valid = $true
            if ($defaultChosen) {
                Write-Host "You chose the enforcement date: $($inputDateStr)" -ForegroundColor Green
            } else {
                Write-Host "You entered the enforcement date: $($inputDateStr)" -ForegroundColor Green
            }
            Write-Host
        }
    }

    return $inputDate
}

# Call the function
Set-TenantSettingsMFAPostponement -TenantId $TenantId -PostponementDateInUTC $PostponementDateInUTC