Compartir a través de


Cómo posponer la ejecución para un inquilino donde los usuarios no pueden iniciar sesión después del despliegue del requisito obligatorio de MFA

Es posible que los usuarios no puedan iniciar sesión en Azure Portal, en el Centro de administración de Microsoft Entra o en el Centro de administración de Microsoft Intune si tienen problemas para usar su método MFA después de que se implemente el requisito obligatorio para usar MFA en su inquilino.

Si los usuarios no pueden iniciar sesión, puede ejecutar el siguiente script como administrador global para posponer temporalmente la aplicación de MFA para el inquilino.

Para más información sobre los requisitos obligatorios de MFA de Azure, consulte Planeamiento de la autenticación multifactor obligatoria para Azure y otros portales de administración. El siguiente script solo se aplica a las aplicaciones de la fase 1.

Acciones de script

El script realiza las siguientes acciones:

  • Elige el inquilino del usuario si tiene uno o presenta una lista de inquilinos entre los que elegir. Opcionalmente, el script solicita la fecha de cumplimiento. La fecha predeterminada es el 30 de septiembre de 2025.
  • Registra al usuario en ese inquilino.
  • Obtiene los tokens de autenticación pertinentes.
  • Comprueba si el usuario tiene acceso elevado. Si no es así, el script realiza la elevación.
  • Comprueba si se asigna el rol adecuado al usuario en el proveedor de recursos de configuración (RP). Si no es así, el script asigna el rol adecuado.
  • Actualiza la fecha de cumplimiento en Entra ID.
  • Intenta quitar el acceso con privilegios elevados si el script lo agregó.

Prerrequisitos

Guión

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