Sean
Sean

Reputation: 1456

Blazor Azure B2C standalone not sending authorization header in request (JWT)

I have a Blazor Webassembly app that is using Azure B2C to authenticate and authorize users. I am able to successfully log in and the token is generated, but when I try to call my API, there is no token in the request header.

I followed this guide: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-azure-active-directory-b2c?view=aspnetcore-7.0

Here's my Program.cs

var builder = WebAssemblyHostBuilder.CreateDefault(args);

var baseAddress = builder.Configuration.GetValue<string>("BaseUrl");

builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddHttpClient("WebAPI", client => client.BaseAddress = new Uri(baseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
    .CreateClient("WebAPI"));

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("https://{domain}.onmicrosoft.com/{App ID}/Api.Access");
});

await builder.Build().RunAsync();

Am I missing a line to add the token to the HttpClient?

Upvotes: 0

Views: 902

Answers (2)

Dani
Dani

Reputation: 2036

Following @Tiny Wang answer I have discovered that, if you use BaseAddressAuthorizationMessageHandler it will only send authorization bearer if the call is inside navigationManager.BaseUri:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http;

namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;

/// <summary>
/// A <see cref="DelegatingHandler"/> that attaches access tokens to outgoing <see cref="HttpResponseMessage"/> instances.
/// Access tokens will only be added when the request URI is within the application's base URI.
/// </summary>
public class BaseAddressAuthorizationMessageHandler : AuthorizationMessageHandler
{
    /// <summary>
    /// Initializes a new instance of <see cref="BaseAddressAuthorizationMessageHandler"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for requesting tokens.</param>
    /// <param name="navigationManager">The <see cref="NavigationManager"/> used to compute the base address.</param>
    public BaseAddressAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(new[] { navigationManager.BaseUri });
    }
}

You have to create a custom handler like this and specify the domains where you want to include authorization bearer:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http;

namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;

/// <summary>
/// A <see cref="DelegatingHandler"/> that attaches access tokens to outgoing <see cref="HttpResponseMessage"/> instances.
/// Access tokens will only be added when the request URI is within the application's base URI.
/// </summary>
public class BaseAddressAuthorizationMessageHandler_Custom : AuthorizationMessageHandler
{
    /// <summary>
    /// Initializes a new instance of <see cref="BaseAddressAuthorizationMessageHandler"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for requesting tokens.</param>
    /// <param name="navigationManager">The <see cref="NavigationManager"/> used to compute the base address.</param>
    public BaseAddressAuthorizationMessageHandler_Custom(IAccessTokenProvider provider, NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(new[] { navigationManager.BaseUri, "https://localhost:7039" ❗❗❗ });
    }
}

And then use in your program like this:

builder.Services.AddScoped<BaseAddressAuthorizationMessageHandler_Custom>();

builder.Services.AddHttpClient<IEmployeeDataService, EmployeeDataService>(client => client.BaseAddress =
new Uri("https://localhost:7039")).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler_Custom>();
builder.Services.AddHttpClient<ICountryDataService, CountryDataService>(client => client.BaseAddress =
new Uri("https://localhost:7039")).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler_Custom>();
builder.Services.AddHttpClient<IJobCategoryDataService, JobCategoryDataService>(client => client.BaseAddress =
new Uri("https://localhost:7039")).AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler_Custom>();

Then, in server side, be sure to allow the origin for avoid CORS errors:

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: "_myAllowSpecificOrigins",
                      policy =>
                      {

                          policy.WithOrigins("http://localhost:5109")
                          .AllowAnyHeader()
                          .AllowAnyMethod();
                      });
});

...

app.UseRouting();

app.UseCors("_myAllowSpecificOrigins");

app.UseAuthentication();
app.UseAuthorization();

Upvotes: 0

Tiny Wang
Tiny Wang

Reputation: 16066

I had a blazor app which is https://localhost:7280 and it doesn't contain an API. And I also had another api project which had API https://localhost:7018/WeatherForecast. Then when I write code like this:

builder.Services.AddHttpClient("WebAPI",client => client.BaseAddress = new Uri("https://localhost:7018"))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

result = await Http.GetStringAsync("/WeatherForecast");

the request would send without bearer token in the request header and would get 401 error.

enter image description here

And we can also see the token is already generated.

enter image description here

But when I write code like this:

builder.Services.AddHttpClient("WebAPI",client => client.BaseAddress = new Uri("https://localhost:7280"))
        .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
    
    result = await Http.GetStringAsync("/WeatherForecast");

There would be an access token inside the request.

enter image description here

I'm afraid this is what this sentence means.... So we can only manually generate the token and add it in request header like what I shared below.

enter image description here

==================================================

I had a test in my side and I met your issue in my side as well and this is what I used to generate token manually to call the api. use the code I commented.

@page "/profile"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
@attribute [Authorize]
@inject IAccessTokenProvider TokenProvider
@inject HttpClient Http

<h3>User Profile</h3>
<button @onclick="call">
    call api
</button>
<div>@result</div>

@code {

    private string result = "no data now";

    private async Task call()
    {
        try
        {
            result = await Http.GetStringAsync("https://localhost:7018/WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }

        //var http = new HttpClient();
        //http.BaseAddress = new Uri("https://localhost:7018/WeatherForecast");

        //var tokenResult = await TokenProvider.RequestAccessToken(
        //    new AccessTokenRequestOptions
        //        {
        //            Scopes = new[] { "api://xxxx/Tiny.Read" }
        //        });

        //if (tokenResult.TryGetToken(out var token))
        //{
        //    http.DefaultRequestHeaders.Add("Authorization",
        //        $"Bearer {token.Value}");
        //    result = await http.GetStringAsync("https://localhost:7018/WeatherForecast");
        //}
    }
}

enter image description here enter image description here

Upvotes: 2

Related Questions