How to inject access token to HttpClient in Service registration in Blazor server app?

Viewed 2401

I have Blazor server app to use Identity Server 4 for authentication and authorization purposes. And, I have a protected api (JWT token) to provide data to Blazor server app.

I have followed this post to get access token and pass it to HttpClient during service registration as below,

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAntDesign();

    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddScoped<TokenProvider>(); <--
    ApplicationInitializer.Initialize(Configuration, services);
}

ApplicationInitializer.cs

public static class ApplicationInitializer
{
    public static void Initialize(IConfiguration configuration, IServiceCollection services)
    {
        var installers = typeof(Startup).Assembly.ExportedTypes
            .Where(w => typeof(IInstaller).IsAssignableFrom(w) && !w.IsInterface && !w.IsAbstract)
            .Select(Activator.CreateInstance)
            .Cast<IInstaller>()
            .ToList();

        installers.ForEach(installer => installer.InstallServices(services, configuration));
    }
}

ServiceCollectionRegistration.cs

public class ServiceCollectionRegistration : IInstaller
{
    public void InstallServices(IServiceCollection services, IConfiguration configuration)
    {
        //Register api (NSwag generated) clients with HttpClient from HttpClientFactory
        var apiOptions = new ApiOptions();
        configuration.GetSection(nameof(ApiOptions)).Bind(apiOptions);
        services.AddHttpClient("api", (provider, client) =>
        {
            client.BaseAddress = new Uri(apiOptions.ApiUrl);

            //This is not working as expected. Access Token is always null
            var tokenProvider = services.BuildServiceProvider().GetRequiredService<TokenProvider>();
            var accessToken = tokenProvider.AccessToken;
            if (accessToken != null)
                client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);
        });

        var asm = Assembly.GetExecutingAssembly();
        var interfaces = asm.GetInterfaces();
        foreach (var interfaceType in interfaces.Where(x => x.Name.EndsWith("Client")))
        {
            var currentInterfaceType = interfaceType;
            var implementations = asm.GetImplementationsOfInterface(interfaceType);
            implementations.ToList().ForEach(i =>
            {
                services.AddScoped(currentInterfaceType, ctx =>
                {
                    var clientFactory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
                    var httpClient = clientFactory.CreateClient("api");
                    return Activator.CreateInstance(i, httpClient);
                });
            });
        }

        //Register all provider type to their interface type using reflection
        foreach (var interfaceType in interfaces.Where(x => !x.Name.EndsWith("Client")))
        {
            var currentInterfaceType = interfaceType;
            var implementations = asm.GetImplementationsOfInterface(interfaceType);
            if (implementations.Count > 1)
                implementations.ToList().ForEach(i => services.AddScoped(currentInterfaceType, i));
            else
                implementations.ToList().ForEach(i => services.AddScoped(currentInterfaceType, i));
        }
        new AutoMapperConfiguration().RegisterProfiles(services);
    }
}

I am unable to assign access token to HttpClient object since it was null always. Did I miss anything?

1 Answers

I am unable to assign access token to HttpClient object since it was null always.

Since the token is to be accessed from the current HttpContext during a request, it wont be available at the time when registering the client.

This makes trying to add a default header not ideal.

Consider changing the approach.

Create a message handler to intercept/extract and inject the desired header during the scope of a request

public class TokenHandler : DelegatingHandler {
    private readonly IHttpContextAccessor accessor;
    
    public TokenHandler(IHttpContextAccessor accessor) => this.accessor = accessor;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        //get the token
        var accessToken = await accessor.HttpContext.GetTokenAsync("access_token");
        //add header
        request.Headers.Authorization =
            new AuthenticationHeaderValue("Bearer", accessToken);
        //continue down stream request
        return await base.SendAsync(request, cancellationToken);
    }
}

include the message handler in the pipeline when registering the client, like in this simplified example for adding the API named client

public void ConfigureServices(IServiceCollection services) {
    services.AddAntDesign();

    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddHttpContextAccessor();
    services.AddScoped<TokenHandler>();
    
    //Register api (NSwag generated) clients with HttpClient from HttpClientFactory
    ApiOptions apiOptions = Configuration.GetSection(nameof(ApiOptions)).Get<ApiOptions>();
    services
        .AddHttpClient("api", (client) => {
            client.BaseAddress = new Uri(apiOptions.ApiUrl);
        })
        .AddHttpMessageHandler<TokenHandler>(); //inject token using our token handler
    
    //...
}

The ServiceCollectionRegistration.InstallServices is overly complicated and will be difficult to maintain (IMO) with it Separation of Concerns (SoC) violations and improperly calling BuildServiceProvider that will cause problems later on.

Use the provider in the factory delegate when registering your types so the proper IServiceProvider is used to resolve service dependencies.

//...

foreach (var interfaceType in interfaces.Where(x => x.Name.EndsWith("Client"))) {
    var currentInterfaceType = interfaceType;
    var implementations = asm.GetImplementationsOfInterface(interfaceType);
    implementations.ToList().ForEach(i => {
        services.AddScoped(currentInterfaceType, provider => {
            var clientFactory = provider.GetRequiredService<IHttpClientFactory>();
            var httpClient = clientFactory.CreateClient("api");
            return Activator.CreateInstance(i, httpClient);
        });
    });
}

//...
Related