max-ieremenko / ServiceModel.Grpc

Code-first for gRPC
https://max-ieremenko.github.io/ServiceModel.Grpc/
Apache License 2.0
90 stars 11 forks source link

[Question] What is the correct way to update the value of Authorization header with every new gRPC call? #150

Closed alphajoza closed 1 year ago

alphajoza commented 1 year ago

Context: .net 7.0

My server side is done in asp.net core and the client side is done in Blazer webassembly as following (Program.cs):

...
builder.Services.AddSingleton(provider =>
{
    var baseAddress = new Uri("http://localhost:40000");
    var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWebText, new HttpClientHandler());
    return GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions { HttpHandler = httpHandler });
});

builder.Services.AddScoped<IClientFactory>(serviceProvider =>
{
    //this runs only once and not for every call
    var provider = serviceProvider.GetRequiredService<TokenProviderService>();
    var token = provider.GetToken();

    return new ClientFactory(new ServiceModelGrpcClientOptions
    {
        DefaultCallOptionsFactory = () => new CallOptions(new Metadata
        {
            { "Authorization", token }
        })
    });
});

builder.Services.AddScoped(provider =>
{
    var channel = provider.GetRequiredService<GrpcChannel>();
    return provider.GetRequiredService<IClientFactory>().CreateClient<IGrpcContract>(channel);
});
...

The implementation of TokenProviderService is such that it uses the AuthenticationStateProviderService for setting the various states of authentication (anonymous, authenticated):

public class TokenProviderService
{
    private readonly AuthenticationStateProviderService _authenticationStateProviderService;

    public TokenProviderService(AuthenticationStateProviderService authenticationStateProviderService)
    {
        _authenticationStateProviderService = authenticationStateProviderService;
    }
    public string GetToken()
    {
        return _authenticationStateProviderService.IsAuthenticated ? _authenticationStateProviderService.Token! : "";
    }
}

Obviously, the application (Blazor wasm) starts as a anonymous user and then gets authenticated. My problem is that the process of setting the Authorization header is done only once when creating the ClientFactory.

How can I make sure that for every grpc request, GetToken() gets called or recreate the ClientFactory when authentication state changes (anonymous becomes authenticated)?

max-ieremenko commented 1 year ago
services.AddScoped<IClientFactory>(serviceProvider =>
{
    // new CallOptions for each grpc call
    CallOptions DefaultCallOptionsFactory()
    {
        var provider = serviceProvider.GetRequiredService<TokenProviderService>();
        var token = provider.GetToken();

        var metadata = new Metadata();
        if (!string.IsNullOrEmpty(token))
        {
            metadata.Add("Authorization", token);
        }

        return new CallOptions(metadata);
    }

    return new ClientFactory(new ServiceModelGrpcClientOptions
    {
        DefaultCallOptionsFactory = DefaultCallOptionsFactory
    });
});

as an alternative, you can try passing token via the custom HttpClientHandler:

// TokenProviderService lifetime should match GrpcChannel lifetime
services.AddSingleton<TokenProviderService>();

// GrpcChannel lifetime should match TokenProviderService lifetime
services.AddSingleton(provider =>
{
    var baseAddress = provider.GetRequiredService<IWebAssemblyHostEnvironment>().BaseAddress;
    var tokenProvider = provider.GetRequiredService<TokenProviderService>();

    var httpHandler = new GrpcWebHandler(
            GrpcWebMode.GrpcWebText,
            // inject the handler into the chain
            new HttpClientHandlerWithAuthorization(tokenProvider));

    return GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions { HttpHandler = httpHandler });
});

// ClientFactory with default options
builder.Services.AddScoped<IClientFactory, ClientFactory>();

internal sealed class HttpClientHandlerWithAuthorization : HttpClientHandler
{
    private readonly TokenProviderService _tokenProvider;

    public HttpClientHandlerWithAuthorization(TokenProviderService tokenProvider)
    {
        _tokenProvider = tokenProvider;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // pass the token for each http call
        var token = _tokenProvider.GetToken();
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }

        return base.SendAsync(request, cancellationToken);
    }
}
alphajoza commented 1 year ago

I used an Interceptor for adding the header, and it worked just fine.

Your solution is working fine too.

thanks.