Búsqueda de sitios web

Obtener un token de acceso para AzureAD mediante PowerShell y el flujo de inicio de sesión del dispositivo


Introducción

¿Alguna vez ha querido consultar una API que utiliza tokens de acceso de Azure Active Directory (AzureAD) desde un script de PowerShell?

Hay muchas soluciones para esto que utilizan una aplicación en AzureAD y se autentica utilizando su ID de cliente y su secreto. Si tengo una aplicación web o un servicio no interactivo, este es el camino a seguir. Mi amigo y colega Emanuel Palm escribió una excelente publicación sobre Microsoft Graph API con PowerShell para ese escenario.

En esta publicación, cubriré cómo obtener un token de acceso de AzureAD mediante el flujo de concesión de autorización de dispositivo OAuth 2.0. Este flujo es excelente cuando quiero que mi secuencia de comandos se ejecute de forma interactiva con un usuario presente. Depende de los derechos de acceso del usuario y no necesito guardar ningún secreto de aplicación en el script. Esto también garantiza que el script no realizará ninguna acción para la que el usuario no tenga acceso.

Esta técnica brindará al usuario una experiencia similar a la del uso de Connect-AzAccount en PowerShell, donde se le hace la siguiente pregunta:

To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ARX2JKHNC to authenticate.

También cubriré un pequeño "truco" para simplificar este flujo para un usuario interactivo, de modo que el usuario no tenga que copiar y pegar el código en un navegador, sino que dependa de crear y abrir un archivo HTML temporal en el navegador predeterminado llamando a Start-Process.

Iniciar un flujo de inicio de sesión del dispositivo

Para iniciar un flujo de inicio de sesión utilizando el flujo de concesión de autorización del dispositivo, debemos llamar al método REST POST a un URI que se parece a este:

"https://login.microsoftonline.com/$TenantID/oauth2/devicecode"

$TenantID puede ser el ID de un inquilino específico o configurarse en una configuración genérica conocida, como "común". Configurar TenantID en "común" utilizará el inquilino donde se creó el usuario. Si quiero iniciar sesión en un inquilino al que estoy invitado como invitado, tendré que proporcionar el TenantID.

Puede leer más sobre las opciones disponibles para TenantID en la documentación sobre opciones de configuración de la aplicación.

Al llamar al método REST, necesito proporcionar los dos parámetros client_id y resource en el cuerpo.

La identificación del cliente es la identificación de una aplicación que tiene permisos delegados para realizar lo que quiero que haga mi script. En este caso, voy a enumerar algunos grupos y, para ello, tomaré prestado el ID de cliente de la aplicación Azure PowerShell que utiliza el módulo Azure PowerShell.

El recurso define dónde será válido mi token de acceso. En mi caso, usaré el token para acceder a la API de Microsoft Graph, por lo que usaré "https://graph.microsoft.com/" como recurso.

Ahora probemos esto en PowerShell:

$ClientID = '1950a258-227b-4e31-a9cf-717495945fc2'
$TenantID = 'common'
$Resource = "https://graph.microsoft.com/"

$DeviceCodeRequestParams = @{
    Method = 'POST'
    Uri    = "https://login.microsoftonline.com/$TenantID/oauth2/devicecode"
    Body   = @{
        client_id = $ClientId
        resource  = $Resource
    }
}

$DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams
Write-Host $DeviceCodeRequest.message -ForegroundColor Yellow

Esto iniciará un nuevo proceso de inicio de sesión y me dará una respuesta que contiene un mensaje que imprimo en la consola con instrucciones para el usuario. La respuesta también contiene las propiedades user_code y device_code que usaremos más adelante.

La experiencia de inicio de sesión

Ahora le he mostrado un mensaje al usuario que le indica que vaya a https://microsoft.com/devicelogin e ingrese un código en una página que se parece a esta:

Después de ingresar el código, se le pedirá al usuario que inicie sesión en mi aplicación, en este caso, "Microsoft Azure PowerShell".

Una vez que haya iniciado sesión, el usuario recibirá un mensaje de confirmación indicándole que cierre el navegador.

Obteniendo la ficha

Una vez que el usuario haya completado el proceso de inicio de sesión, mi secuencia de comandos deberá recuperar el token de acceso. Esto se hace llamando a la API REST una vez más, esta vez utilizando el punto final del token. Aquí hay un ejemplo:

$TokenRequestParams = @{
    Method = 'POST'
    Uri    = "https://login.microsoftonline.com/$TenantId/oauth2/token"
    Body   = @{
        grant_type = "urn:ietf:params:oauth:grant-type:device_code"
        code       = $DeviceCodeRequest.device_code
        client_id  = $ClientId
    }
}
$TokenRequest = Invoke-RestMethod @TokenRequestParams

Si todo salió bien, ahora tendré mi token de acceso en la propiedad access_token de la respuesta.

Obtenga grupos de AzureAD mediante Microsoft Graph API

Para usar nuestro token para autenticarnos en Microsoft Graph API, necesitamos usar un encabezado llamado Autorización y darle el valor de "Portador" seguido de nuestro token.

A continuación se muestra un ejemplo de cómo obtener el primer grupo de mi inquilino mediante Graph API:

$Token = $TokenRequest.access_token
$AadGroupRequestParams = @{
    Method  = 'GET'
    Uri     = 'https://graph.microsoft.com/v1.0/groups?$top=1'
    Headers = @{
        'Authorization' = "Bearer $Token" 
    }
}
$AadGroupRequest = Invoke-RestMethod @AadGroupRequestParams
$AadGroupRequest.value

¡Y eso es! ¡Pero espera, hay más!

Mejorando la experiencia

El uso del flujo de código del dispositivo funciona muy bien para cualquier script que ejecute un usuario interactivo, pero creo que la experiencia del usuario es un poco complicada. Preferiría omitir los primeros tres pasos: abrir un navegador, navegar a https://microsoft.com/devicelogin e ingresar el código del dispositivo.

¡Veamos qué puedo hacer al respecto!

Después de depurar un poco el navegador, descubrí que si uso el método REST POST y proporciono el código en un parámetro llamado "otc" al URI https://login.microsoftonline.com/common/oauth2/deviceauth, lo omitiré. los pasos donde el usuario debe ingresar el código. Sin embargo, abrir un navegador e indicarle que PUBLICAR un mensaje en un sitio no fue tan fácil como esperaba al principio. Mi solución es crear un código HTML, colocarlo en un archivo temporal en el disco y luego abrir ese archivo, con la esperanza de que se abra en el navegador predeterminado del usuario. Mi código HTML contiene un formulario con el parámetro "otc" y un pequeño javascript que envía el formulario mediante una solicitud POST. Puede intentar esto ejecutando la función completa a continuación usando el parámetro "-Interactive".

Una función completa

Por supuesto, he combinado este código en una función lista para ser utilizada. ¡Pruébalo y cuéntame lo que piensas!

<#
.SYNOPSIS
Gets an access token from Azure Active Directory

.DESCRIPTION
Gets an access token from Azure Active Directory that can be used to authenticate to for example Microsoft Graph or Azure Resource Manager.

Run without parameters to get an access token to Microsoft Graph and the users original tenant.

Use the parameter -Interactive and the script will open the sign in experience in the default browser without user having to copy any code.

.PARAMETER ClientID
Application client ID, defaults to well-known ID for Microsoft Azure PowerShell

.PARAMETER Interactive
Tries to open sign-in experience in default browser. If this succeeds the user don't need to copy and paste any device code.

.PARAMETER TenantID
ID of tenant to sign in to, defaults to the tenant where the user was created

.PARAMETER Resource
Identifier for target resource, this is where the token will be valid. Defaults to  "https://graph.microsoft.com/"
Use "https://management.azure.com" to get a token that works with Azure Resource Manager (ARM)

.EXAMPLE
$Token = Connect-AzureDevicelogin -Interactive
$Headers = @{'Authorization' = "Bearer $Token" }
$UsersUri = 'https://graph.microsoft.com/v1.0/users?$top=5'
$Users = Invoke-RestMethod -Method GET -Uri $UsersUri -Headers $Headers
$Users.value.userprincipalname

Using Microsoft Graph to print the userprincipalname of 5 users in the tenant.

.EXAMPLE
$Token = Connect-AzureDevicelogin -Interactive -Resource 'https://management.azure.com'
$Headers = @{'Authorization' = "Bearer $Token" }
$SubscriptionsURI = 'https://management.azure.com/subscriptions?api-version=2019-11-01'
$Subscriptions = Invoke-RestMethod -Method GET -Uri $SubscriptionsURI -Headers $Headers
$Subscriptions.value.displayName

Using Azure Resource Manager (ARM) to print the display name for all the subscriptions the user has access to.

.NOTES

#>
function Connect-AzureDevicelogin {
    [cmdletbinding()]
    param( 
        [Parameter()]
        $ClientID = '1950a258-227b-4e31-a9cf-717495945fc2',
        
        [Parameter()]
        [switch]$Interactive,
        
        [Parameter()]
        $TenantID = 'common',
        
        [Parameter()]
        $Resource = "https://graph.microsoft.com/",
        
        # Timeout in seconds to wait for user to complete sign in process
        [Parameter(DontShow)]
        $Timeout = 300
    )
    try {
        $DeviceCodeRequestParams = @{
            Method = 'POST'
            Uri    = "https://login.microsoftonline.com/$TenantID/oauth2/devicecode"
            Body   = @{
                resource  = $Resource
                client_id = $ClientId
            }
        }
        $DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams

        if ($Interactive.IsPresent) {
            Write-Host 'Trying to open a browser with login prompt. Please sign in.' -ForegroundColor Yellow
            Start-Sleep -Second 1
            $PostParameters = @{otc = $DeviceCodeRequest.user_code }
            $InputFields = foreach ($entry in $PostParameters.GetEnumerator()) {
                "<input type=`"hidden`" name=`"$($entry.Name)`" value=`"$($entry.Value)`">"
            }
            $PostUrl = "https://login.microsoftonline.com/common/oauth2/deviceauth"
            $LocalHTML = @"
        <!DOCTYPE html>
<html>
 <head>
  <title>&hellip;</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <script type="text/javascript">
   function dosubmit() { document.forms[0].submit(); }
  </script>
 </head>
 <body onload="dosubmit();">
  <form action="$PostUrl" method="POST" accept-charset="utf-8">
   $InputFields
  </form>
 </body>
</html>
"@
            $TempPage = New-TemporaryFile
            $TempPage = Rename-Item -Path $TempPage.FullName ($TempPage.FullName -replace '$', '.html') -PassThru    
            Out-File -FilePath $TempPage.FullName -InputObject $LocalHTML
            Start-Process $TempPage.FullName
        }
        else {
            Write-Host $DeviceCodeRequest.message -ForegroundColor Yellow
        }

        $TokenRequestParams = @{
            Method = 'POST'
            Uri    = "https://login.microsoftonline.com/$TenantId/oauth2/token"
            Body   = @{
                grant_type = "urn:ietf:params:oauth:grant-type:device_code"
                code       = $DeviceCodeRequest.device_code
                client_id  = $ClientId
            }
        }
        $TimeoutTimer = [System.Diagnostics.Stopwatch]::StartNew()
        while ([string]::IsNullOrEmpty($TokenRequest.access_token)) {
            if ($TimeoutTimer.Elapsed.TotalSeconds -gt $Timeout) {
                throw 'Login timed out, please try again.'
            }
            $TokenRequest = try {
                Invoke-RestMethod @TokenRequestParams -ErrorAction Stop
            }
            catch {
                $Message = $_.ErrorDetails.Message | ConvertFrom-Json
                if ($Message.error -ne "authorization_pending") {
                    throw
                }
            }
            Start-Sleep -Seconds 1
        }
        Write-Output $TokenRequest.access_token
    }
    finally {
        try {
            Remove-Item -Path $TempPage.FullName -Force -ErrorAction Stop
            $TimeoutTimer.Stop()
        }
        catch {
            # We don't care about errors here
        }
    }
}

Artículos relacionados