簡體   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