Aplicación de escritorio que llama a las API web: adquisición de un token mediante el flujo de código de dispositivo

Si va a escribir una herramienta de línea de comandos que no tenga controles web y no puede o no quiere usar los flujos anteriores, use el flujo de código de dispositivo.

Flujo de código de dispositivo

La autenticación interactiva con Microsoft Entra ID requiere un explorador web. Para más información, consulte Uso de exploradores web. Para autenticar a los usuarios en dispositivos o sistemas operativos que no proporcionan un explorador web, el flujo de código de dispositivo permite al usuario usar otro dispositivo, como un equipo o un teléfono móvil, para iniciar sesión de forma interactiva. Al usar flujo de código de dispositivo, la aplicación obtiene los tokens mediante un proceso de dos pasos diseñado para estos dispositivos o sistemas operativos. Ejemplos de estas aplicaciones son aquellas que se ejecutan en iOT o herramientas de línea de comandos (CLI). La idea es la siguiente:

  1. Cada vez que se necesita autenticación de usuario, la aplicación proporciona un código para el usuario. Se pide al usuario que use otro dispositivo, como un smartphone conectado a Internet, para ir a una dirección URL, por ejemplo, https://microsoft.com/devicelogin. Luego se pide al usuario que escriba el código. Una vez hecho eso, la página web lleva al usuario a una experiencia de autenticación normal, que incluye peticiones de consentimiento y autenticación multifactor, en caso necesario.

  2. Tras una autenticación correcta, la aplicación de línea de comandos recibe los tokens necesarios a través de un canal posterior y los usa para realizar las llamadas a la API web que necesita.

Uso

IPublicClientApplication contiene un método denominado AcquireTokenWithDeviceCode.

 AcquireTokenWithDeviceCode(IEnumerable<string> scopes,
                            Func<DeviceCodeResult, Task> deviceCodeResultCallback)

Este método toma como parámetros:

  • Los elementos scopes para los que solicitar un token de acceso.
  • Una devolución de llamada que recibe DeviceCodeResult.

El código de ejemplo siguiente presenta la sinopsis de los casos más actuales, con explicaciones de la clase de excepciones que se pueden obtener y su mitigación. Para ver un ejemplo de código totalmente funcional, consulte active-directory-dotnetcore-devicecodeflow-v2 en GitHub.

private const string ClientId = "<client_guid>";
private const string Authority = "https://login.microsoftonline.com/contoso.com";
private readonly string[] scopes = new string[] { "user.read" };

static async Task<AuthenticationResult> GetATokenForGraph()
{
    IPublicClientApplication pca = PublicClientApplicationBuilder
            .Create(ClientId)
            .WithAuthority(Authority)
            .WithDefaultRedirectUri()
            .Build();

    var accounts = await pca.GetAccountsAsync();

    // All AcquireToken* methods store the tokens in the cache, so check the cache first
    try
    {
        return await pca.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
            .ExecuteAsync();
    }
    catch (MsalUiRequiredException ex)
    {
        // No token found in the cache or Azure AD insists that a form interactive auth is required (e.g. the tenant admin turned on MFA)
        // If you want to provide a more complex user experience, check out ex.Classification

        return await AcquireByDeviceCodeAsync(pca);
    }
}

private static async Task<AuthenticationResult> AcquireByDeviceCodeAsync(IPublicClientApplication pca)
{
    try
    {
        var result = await pca.AcquireTokenWithDeviceCode(scopes,
            deviceCodeResult =>
            {
                    // This will print the message on the console which tells the user where to go sign-in using
                    // a separate browser and the code to enter once they sign in.
                    // The AcquireTokenWithDeviceCode() method will poll the server after firing this
                    // device code callback to look for the successful login of the user via that browser.
                    // This background polling (whose interval and timeout data is also provided as fields in the
                    // deviceCodeCallback class) will occur until:
                    // * The user has successfully logged in via browser and entered the proper code
                    // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached
                    // * The developing application calls the Cancel() method on a CancellationToken sent into the method.
                    //   If this occurs, an OperationCanceledException will be thrown (see catch below for more details).
                    Console.WriteLine(deviceCodeResult.Message);
                return Task.FromResult(0);
            }).ExecuteAsync();

        Console.WriteLine(result.Account.Username);
        return result;
    }

    // TODO: handle or throw all these exceptions depending on your app
    catch (MsalServiceException ex)
    {
        // Kind of errors you could have (in ex.Message)

        // AADSTS50059: No tenant-identifying information found in either the request or implied by any provided credentials.
        // Mitigation: as explained in the message from Azure AD, the authoriy needs to be tenanted. you have probably created
        // your public client application with the following authorities:
        // https://login.microsoftonline.com/common or https://login.microsoftonline.com/organizations

        // AADSTS90133: Device Code flow is not supported under /common or /consumers endpoint.
        // Mitigation: as explained in the message from Azure AD, the authority needs to be tenanted

        // AADSTS90002: Tenant <tenantId or domain you used in the authority> not found. This may happen if there are
        // no active subscriptions for the tenant. Check with your subscription administrator.
        // Mitigation: if you have an active subscription for the tenant this might be that you have a typo in the
        // tenantId (GUID) or tenant domain name.
    }
    catch (OperationCanceledException ex)
    {
        // If you use a CancellationToken, and call the Cancel() method on it, then this *may* be triggered
        // to indicate that the operation was cancelled.
        // See https://learn.microsoft.com/dotnet/standard/threading/cancellation-in-managed-threads
        // for more detailed information on how C# supports cancellation in managed threads.
    }
    catch (MsalClientException ex)
    {
        // Possible cause - verification code expired before contacting the server
        // This exception will occur if the user does not manage to sign-in before a time out (15 mins) and the
        // call to `AcquireTokenWithDeviceCode` is not cancelled in between
    }
}

Pasos siguientes

Avance al siguiente artículo de este escenario, Llamada a una API web desde la aplicación de escritorio.