2

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?

Balagurunathan Marimuthu
  • 2,927
  • 4
  • 31
  • 44

1 Answers1

3

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

//...
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thanks for your brief explanation and it saved my day! And, I modified the code as you suggested especially `ServiceCollectionRegistration.InstallServices` Cheers! – Balagurunathan Marimuthu Feb 23 '21 at 05:54
  • I have a question... Which lifetime I need to use to register `TokenHandler` in `ServiceCollection`. I used `services.AddScoped();` but it didn't work everytime. – Balagurunathan Marimuthu Feb 24 '21 at 07:49
  • @BalagurunathanMarimuthu Scoped is fine. What do you mean it did not work every time? Can you clarify? – Nkosi Feb 24 '21 at 10:47
  • It is not working in production environment. I searched about and I got this... https://stackoverflow.com/a/60269062/4337436 Please read the comments from MichaelB & The Muffin Man. I enabled the logger and investigated and found that it thrown `null reference` issue at `await accessor.HttpContext.GetTokenAsync("access_token")` And, I followed this [docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios?view=aspnetcore-5.0) and I got null when I access token. Please help me to solve this. – Balagurunathan Marimuthu Feb 25 '21 at 15:59
  • 1
    thanks for that, pretty much what i was searching for. im coming from autorest (legacy) in .net framework apps and this is my first time using the new open api service reference in a .net 5 api. For us, this is just to have apis talking to other apis using client credentials grant. Should be easy enough to add my existing code to get the token instead of getting it from httpcontext – punkologist Aug 18 '21 at 03:18