如果你正在编写一个不包含 Web 控件的命令行工具,并且无法或者不想要使用前面所述的流,请使用设备代码流。

设备代码流

使用 Azure AD 的交互式身份验证需要 Web 浏览器。 有关详细信息,请参阅 Web 浏览器的用法 。 为了对不提供 Web 浏览器的设备或操作系统上的用户进行身份验证,设备代码流可让用户使用另一台设备(例如某台计算机或手机)以交互方式登录。 通过使用设备代码流,应用程序将通过专为这些设备或操作系统设计的两步过程获取令牌。 此类应用程序的例子包括 iOT 上运行的应用程序或命令行工具 (CLI)。 其思路是:

  • 每当需要用户身份验证时,应用就会为用户提供一个代码。 系统要求用户使用另一台设备(例如,已连接到 Internet 的智能手机)转到某个 URL(例如 https://microsoft.com/devicelogin )。 然后系统会提示用户输入代码。 完成此操作后,网页将引导用户进行常规的身份验证操作,其中包括同意提示和多重身份验证(如有必要)。

  • 身份验证成功后,命令行应用通过后端通道接收所需的令牌,并使用它们来执行所需的 Web API 调用。

    macOS Node.js Python

    IPublicClientApplication 包含名为 AcquireTokenWithDeviceCode 的方法。

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

    该方法用作参数:

  • 要请求其访问令牌的 scopes
  • 接收 DeviceCodeResult 的回调。
  • 以下示例代码显示了最新事例的概要,并解释了可能出现的各种异常及其缓解措施。 有关完整的功能代码示例,请参阅 GitHub 上的 active-directory-dotnetcore-devicecodeflow-v2

    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
            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)
            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
    

    此代码摘录自 MSAL Java 代码示例

     private static IAuthenticationResult acquireTokenDeviceCode() throws Exception {
            // Load token cache from file and initialize token cache aspect. The token cache will have
            // dummy data, so the acquireTokenSilently call will fail.
            TokenCacheAspect tokenCacheAspect = new TokenCacheAspect("sample_cache.json");
            PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID)
                    .authority(AUTHORITY)
                    .setTokenCacheAccessAspect(tokenCacheAspect)
                    .build();
            Set<IAccount> accountsInCache = pca.getAccounts().join();
            // Take first account in the cache. In a production application, you would filter
            // accountsInCache to get the right account for the user authenticating.
            IAccount account = accountsInCache.iterator().next();
            IAuthenticationResult result;
            try {
                SilentParameters silentParameters =
                        SilentParameters
                                .builder(SCOPE, account)
                                .build();
                // try to acquire token silently. This call will fail since the token cache
                // does not have any data for the user you are trying to acquire a token for
                result = pca.acquireTokenSilently(silentParameters).join();
            } catch (Exception ex) {
                if (ex.getCause() instanceof MsalException) {
                    Consumer<DeviceCode> deviceCodeConsumer = (DeviceCode deviceCode) ->
                            System.out.println(deviceCode.message());
                    DeviceCodeFlowParameters parameters =
                            DeviceCodeFlowParameters
                                    .builder(SCOPE, deviceCodeConsumer)
                                    .build();
                    // Try to acquire a token via device code flow. If successful, you should see
                    // the token and account information printed out to console, and the sample_cache.json
                    // file should have been updated with the latest tokens.
                    result = pca.acquireToken(parameters).join();
                } else {
                    // Handle other exceptions accordingly
                    throw ex;
            return result;
    const deviceCodeRequest = {
        deviceCodeCallback: (response) => (console.log(response.message)),
        scopes: ["user.read"],
        timeout: 20,
    pca.acquireTokenByDeviceCode(deviceCodeRequest).then((response) => {
        console.log(JSON.stringify(response));
    }).catch((error) => {
        console.log(JSON.stringify(error));
    

    此代码摘录自 MSAL Python 开发示例

    # Create a preferably long-lived app instance which maintains a token cache.
    app = msal.PublicClientApplication(
        config["client_id"], authority=config["authority"],
        # token_cache=...  # Default cache is in memory only.
                           # You can learn how to use SerializableTokenCache from
                           # https://msal-python.rtfd.io/en/latest/#msal.SerializableTokenCache
    # The pattern to acquire a token looks like this.
    result = None
    # Note: If your device-flow app does not have any interactive ability, you can
    #   completely skip the following cache part. But here we demonstrate it anyway.
    # We now check the cache to see if we have some end users signed in before.
    accounts = app.get_accounts()
    if accounts:
        logging.info("Account(s) exists in cache, probably with token too. Let's try.")
        print("Pick the account you want to use to proceed:")
        for a in accounts:
            print(a["username"])
        # Assuming the end user chose this one
        chosen = accounts[0]
        # Now let's try to find a token in cache for this account
        result = app.acquire_token_silent(config["scope"], account=chosen)
    if not result:
        logging.info("No suitable token exists in cache. Let's get a new one from Azure AD.")
        flow = app.initiate_device_flow(scopes=config["scope"])
        if "user_code" not in flow:
            raise ValueError(
                "Fail to create device flow. Err: %s" % json.dumps(flow, indent=4))
        print(flow["message"])
        sys.stdout.flush()  # Some terminal needs this to ensure the message is shown
        # Ideally you should wait here, in order to save some unnecessary polling
        # input("Press Enter after signing in from another device to proceed, CTRL+C to abort.")
        result = app.acquire_token_by_device_flow(flow)  # By default it will block
            # You can follow this instruction to shorten the block time
            #    https://msal-python.readthedocs.io/en/latest/#msal.PublicClientApplication.acquire_token_by_device_flow
            # or you may even turn off the blocking behavior,
            # and then keep calling acquire_token_by_device_flow(flow) in your own customized loop
    

    转到此方案中的下一篇文章:从桌面应用调用 Web API

  •