Blazor Standalone WASM 无法使用 MSAL 获取访问令牌

Blazor Standalone WASM Unable to get Access Token with MSAL

折腾了2天,投入了6h左右,终于决定求助了。

我有一个带有 MSAL 身份验证的独立 Blazor WASM 应用程序,在登录成功并尝试获取访问令牌后我收到错误消息:

blazor.webassembly.js:1 info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[1]
      Authorization was successful.
blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: An exception occurred executing JS interop: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 73.. See InnerException for more details.
Microsoft.JSInterop.JSException: An exception occurred executing JS interop: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 73.. See InnerException for more details.
 ---> System.Text.Json.JsonException: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 73.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'Null' as a string.
   at System.Text.Json.Utf8JsonReader.TryGetDateTimeOffset(DateTimeOffset& value)
   at System.Text.Json.Utf8JsonReader.GetDateTimeOffset()

此错误仅在我登录后显示。

我的设置是 运行 在 .NET 5.0 上,身份验证提供程序是一个 Azure B2C 租户,我将重定向 URI 正确配置为“单页应用程序”并授予“offline_access”和“openid”。

这是我的Program.cs

public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            // Authenticate requests to Function API
            builder.Services.AddScoped<APIFunctionAuthorizationMessageHandler>();
            
            //builder.Services.AddHttpClient("MyAPI", 
            //    client => client.BaseAddress = new Uri("<https://my_api_uri>"))
            //  .AddHttpMessageHandler<APIFunctionAuthorizationMessageHandler>();

            builder.Services.AddMudServices();

            builder.Services.AddMsalAuthentication(options =>
            {
                // Configure your authentication provider options here.
                // For more information, see https://aka.ms/blazor-standalone-auth
                builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);

                options.ProviderOptions.LoginMode = "redirect";
                options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
                options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
            });

            await builder.Build().RunAsync();
        }
    }

我特意将 HTTPClient link 注释掉到 AuthorizationMessageHandler。 “AzureAD”配置具有设置为 true 的 Authority、ClientId 和 ValidateAuthority。

public class APIFunctionAuthorizationMessageHandler : AuthorizationMessageHandler
    {
        public APIFunctionAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
        {
            ConfigureHandler(
                authorizedUrls: new[] { "<https://my_api_uri>" });
                //scopes: new[] { "FunctionAPI.Read" });
        }
    }

我已经尝试定义范围,例如 openid 或自定义 API 范围,但现在没有。没有区别。

那么为了引起异常,我所做的只是一些简单的事情:

@code {
    private string AccessTokenValue;

    protected override async Task OnInitializedAsync()
    {
        var accessTokenResult = await TokenProvider.RequestAccessToken();
        AccessTokenValue = string.Empty;

        if (accessTokenResult.TryGetToken(out var token))
        {
            AccessTokenValue = token.Value;
        }
    }
}

最后的objective就是这样使用:

   try {
      var httpClient = ClientFactory.CreateClient("MyAPI");
      var resp = await httpClient.GetFromJsonAsync<APIResponse>("api/Function1");
      FunctionResponse = resp.Value;
      Console.WriteLine("Fetched " + FunctionResponse);
   }
   catch (AccessTokenNotAvailableException exception)
   {
      exception.Redirect();
   }

但是返回了同样的错误,甚至在运行之前看起来也是如此。 此代码也是 Blazor 组件的 OnInitializedAsync()。

欢迎提出任何想法或建议。 我被困住了,有点绝望。

我怀疑没有从 Azure AD B2C 请求或返回访问令牌,但假设这是 AuthorizationMessageHandler 作业。

不胜感激。

谢谢。

找到问题。

在 JavaScript 端进行了一些调试后,文件 AuthenticationService.js,第 171 行的方法“async getTokenCore(e)”经过美化后,我确认访问令牌实际上是未被 returned,仅 IdToken。

阅读这篇关于向 Azure AD B2C 请求访问令牌的文档时,它提到根据您定义的范围,它将 return 返回给您。

范围“openid”告诉它你需要一个 IdToken,然后“offline_access”告诉它你需要一个刷新令牌,最后有一个漂亮的技巧,你可以定义 App Id 的范围和它将 return 一个访问令牌。 此处有更多详细信息:https://docs.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#openid-connect-scopes

所以我在 Program.cs、builder.Services.AddMsalAuthentication 步骤中更改了我的代码。

现在看起来像这样:

builder.Services.AddMsalAuthentication(options =>
            {
                // Configure your authentication provider options here.
                // For more information, see https://aka.ms/blazor-standalone-auth
                builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);

                options.ProviderOptions.LoginMode = "redirect";
                options.ProviderOptions.DefaultAccessTokenScopes.Add("00000000-0000-0000-0000-000000000000");
                //options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
                //options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
            });

我设置了我在此 Blazor 应用程序上使用的实际应用程序 ID,而不是“00000000-0000-0000-0000-000000000000”。

现在错误没有发生并且访问令牌 returned.

谢谢。

我也很难找到高质量的例子。以下是我如何解决从 Webassembly(托管或独立)应用程序调用 1 个或多个 API 的问题。

大多数 MSFT 示例仅处理一个 Api,因此在通过 AddMsalAuthentication 注册 Msal 时使用 options.ProviderOptions.DefaultAccessTokenScopes 选项。这会将您的令牌锁定到单个受众,当您有多个 api 可以调用时,这不起作用。

相反,从 AuthorizationMessageHandler class 为每个 api 端点派生一个处理程序,在 ConfigureHandler 中设置 authorizedUrl 范围,注册名为 HttpClient 的对于 DI 容器中的每个端点,并使用 IHttpClientFactory 生成 HttpClient。

场景: 假设我有一个 WebAssembly 应用程序(托管或独立),它调用多个受保护的 api,包括微软图形 api.

首先,我必须为每个派生自 AuthorizationRequestMessageHandler 的 api 创建一个 class:

Api 1:

// This message handler handles calls to the api at the endpoint  "https://localhost:7040".  It will generate tokens with the right audience and scope
// "aud": "api://aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
// "scp": "access_as_user",
public class ApiOneAuthorizationRequestMessageHandler : AuthorizationMessageHandler
{
    // ILogger if you want..
    private readonly ILogger<ApiOneAuthorizationRequestMessageHandler> logger = default!;
    public ApiOneAuthorizationRequestMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager,
        ILoggerFactory loggerFactory
        )
        : base(provider, navigationManager)
    {
        logger = loggerFactory.CreateLogger<ApiOneAuthorizationRequestMessageHandler>() ?? throw new ArgumentNullException(nameof(logger));

        logger.LogDebug($"Setting up {nameof(ApiOneAuthorizationRequestMessageHandler)} to authorize the base url: {"https://localhost:7090/"}");
        ConfigureHandler(
           authorizedUrls: new[] { "https://localhost:7040" },
           scopes: new[] { "api://aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/access_as_user" });
    }
}

Api 2:

// This message handler handles calls to the api at the endpoint  "https://localhost:7090".  Check out the scope and audience through https://jwt.io
// "aud": "api://bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
// "scp": "access_as_user",
public class ApiTwoAuthorizationRequestMessageHandler : AuthorizationMessageHandler
{
    public ApiTwoAuthorizationRequestMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager
        )
        : base(provider, navigationManager)
    {
        ConfigureHandler(
           authorizedUrls: new[] { "https://localhost:7090" },
           scopes: new[] { "api://bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/access_as_user" });
    }
}

MS 图 Api:

// This message handler handles calls to Microsoft graph.
// "aud": "00000003-0000-0000-c000-000000000000"
// "scp": "Calendars.ReadWrite email MailboxSettings.Read openid profile User.Read",
public class GraphApiAuthorizationRequestMessageHandler : AuthorizationMessageHandler
{
    public GraphApiAuthorizationRequestMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager
        )
        : base(provider, navigationManager)
    {
        ConfigureHandler(
           authorizedUrls: new[] { "https://graph.microsoft.com" },
           scopes: new[] { "User.Read", "MailboxSettings.Read", "Calendars.ReadWrite" });
    }
}

现在,使用上面的端点 AuthorizationMessageHandler 为每个端点注册一个命名的 HttpClient。在 Program.cs:

中执行此操作

HttpClient 名为“ProductsApi”

//register the AuthorizationRequestMessageHandler
builder.Services.AddScoped<ApiOneAuthorizationRequestMessageHandler>();
//register the named HttpClient 
builder.Services.AddHttpClient("ProductsApi",
    httpClient => httpClient.BaseAddress = new Uri("https://localhost:7040"))
    .AddHttpMessageHandler<ApiOneAuthorizationRequestMessageHandler>();

名为“营销Api”的 HttpClient:

builder.Services.AddScoped<ApiTwoAuthorizationRequestMessageHandler>();
builder.Services.AddHttpClient("MarketingApi",
    httpClient => httpClient.BaseAddress = new Uri("https://localhost:7090"))
    .AddHttpMessageHandler<ApiTwoAuthorizationRequestMessageHandler>();

HttpClient 名为“MSGraphApi”

builder.Services.AddScoped<GraphApiAuthorizationRequestMessageHandler>();
builder.Services.AddHttpClient("MSGraphApi",
    httpClient => httpClient.BaseAddress = new Uri("https://graph.microsoft.com"))
    .AddHttpMessageHandler<GraphApiAuthorizationRequestMessageHandler>();

注册您指定的 HttpClient 后,将 Msal 与您的 AzureAd 应用程序设置一起注册到 Program.cs。

没有客户用户声明的 Msal 注册:

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});

如果您通过图表关注 Microsoft Doc 的自定义用户帐户声明Api,您的添加 Msal 应如下所示:

具有自定义用户声明的 Msal 注册:

builder.Services.AddMsalAuthentication<RemoteAuthenticationState, RemoteUserAccount>(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, GraphUserAccountFactory>();

要使用 GraphServiceClient,需要一个 GraphClientFactory。它将需要使用 IHttpClientFactory 来创建正确命名的 HttpClient(例如 MSGraphApi)。

GraphClientFactory:

public class GraphClientFactory
{
    private readonly IAccessTokenProviderAccessor accessor;
    private readonly IHttpClientFactory httpClientFactory;
    private readonly ILogger<GraphClientFactory> logger;
    private GraphServiceClient graphClient;

    public GraphClientFactory(IAccessTokenProviderAccessor accessor,
        IHttpClientFactory httpClientFactory,
        ILogger<GraphClientFactory> logger)
    {
        this.accessor = accessor;
        this.httpClientFactory = httpClientFactory;
        this.logger = logger;
    }

    public GraphServiceClient GetAuthenticatedClient()
    {
        HttpClient httpClient;

        if (graphClient == null)
        {
            httpClient = httpClientFactory.CreateClient("MSGraphApi");

            graphClient = new GraphServiceClient(httpClient)
            {
                AuthenticationProvider = new GraphAuthProvider(accessor)
            };
        }

        return graphClient;
    }
}

您还需要在 Program.cs 中注册 GraphClientFactory。

builder.Services.AddScoped<GraphClientFactory>();

要访问市场营销 Api,请注入 IHttpClientFactory 并创建一个命名的 HttpClient。

@inject IHttpClientFactory httpClientFactory

<h3>Example Component</h3>

@code {

    protected override async Task OnInitializedAsync()
    {
        try {
            var httpClient = httpClientFactory.CreateClient("MarketingApi");
            var resp = await httpClient.GetFromJsonAsync<APIResponse>("api/Function1");
            FunctionResponse = resp.Value;
            Console.WriteLine("Fetched " + FunctionResponse);
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
    }
}

现在,通过访问 MarketingApi,您还可以使用 Graph Api 通过使用此 MSFT 教程页面中描述的组件来访问您的日历:

Step 4 - Show Calendar Events

访问产品Api与访问营销Api非常相似。

我希望这可以帮助人们在 Blazor Webassembly 中使用正确的访问令牌访问 Api。