繁体   English   中英

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

[英]Blazor Standalone WASM Unable to get Access Token with MSAL

经过2天的战斗,大约投资了6小时,我终于决定寻求帮助。

我有一个带有 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();
        }
    }

我特意注释掉了指向 AuthorizationMessageHandler 的 HTTPClient 链接。 “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;
        }
    }
}

最终目标是使用这样的东西:

   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)”,我已经确认实际上没有返回访问令牌,只有 IdToken。

通过阅读有关向 Azure AD B2C 请求访问令牌的文档,它提到根据您定义的范围,它将更改返回给您的内容。

范围“openid”告诉它您需要一个 IdToken,然后“offline_access”告诉它您需要一个刷新令牌,最后有一个巧妙的技巧,您可以将范围定义为 App Id,它将返回一个访问令牌。 更多详细信息: 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");
            });

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

现在错误没有发生并且访问令牌返回。

谢谢。

我也很难找到高质量的例子。 以下是我解决从 Web 程序集(托管或独立)应用程序调用 1 个或多个 API 的方法。

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

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

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

首先,我必须为从 AuthorizationRequestMessageHandler 派生的每个 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" });
    }
}

接口 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>();

名为“MarketingApi”的 HttpClient:

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

名为“MSGraphApi”的 HttpClient

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

注册命名的 HttpClient 后,将 Msal 与 AzureAd appsettings 一起注册到 Program.cs。

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

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

如果您通过 GraphApi 关注 Microsoft Doc 的自定义用户帐户声明,则您的 Add Msal 应如下所示:

使用自定义用户声明进行 Msal 注册:

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

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

图客户端工厂:

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

要访问 Marketing 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 教程页面中描述的组件:

第 4 步 - 显示日历事件

访问 ProductsApi 与访问 MarketingApi 大致相同。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM