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 YellowEsto 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 @TokenRequestParamsSi 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>…</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
}
}
}

