It's a bit old question already but I encountered the same issue and couldn't find any proper solution.
I was able to make it work, by registering the GraphQL client and custom AuthorizationMessageHandler
like this:
builder.Services.TryAddTransient<GraphQlAuthorizationMessageHandler>();
builder.Services
.AddConferenceClient()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("http://localhost:5000/graphql");
},
configureClientBuilder =>
{
configureClientBuilder.AddHttpMessageHandler<GraphQlAuthorizationMessageHandler>();
});
Where my GraphQlAuthorizationMessageHandler
is simply:
public sealed class GraphQlAuthorizationMessageHandler : AuthorizationMessageHandler
{
public GraphQlAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation)
: base(provider, navigation)
{
base.ConfigureHandler(new [] {"http://localhost:5000/graphql" });
}
}
It works because the existing AuthorizationMessageHandler
(github) class already contains the logic that adds the token to the Authorization header. The only requirement to make it work is to configure the handler with the URLs for which we want it to inject the header.
EDIT:
If you would like to handle both authenticated and unauthenticated users then it will require a little bit more code.
Simply overriding AuthorizationMessageHandler
will not work and you will need to implement your own handler.
As an example, I based my handler on AuthorizationMessageHandler
with the required changes to make it work also for unauthenticated user - in such case, it will not inject the authorization header.
You can implement your own custom logic inside SendAsync
if you would like to add additional checks or maybe use a separate service for providing the authentication state.
public sealed class GraphQlAuthorizationMessageHandlerSettings
{
public IEnumerable<string> AuthorizedUrls { get; set; } = null!;
public IEnumerable<string>? Scopes { get; set; }
public string RedirectUri { get; set; }
}
/// <summary>
/// Entirely based on AuthorizationMessageHandler from Microsoft.AspNetCore.Components.WebAssembly.Authentication
/// src: https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs
/// </summary>
public sealed class GraphQlAuthorizationMessageHandler : DelegatingHandler, IDisposable
{
private readonly IAccessTokenProvider _provider;
private readonly AuthenticationStateChangedHandler _authenticationStateChangedHandler;
private AccessToken _lastToken;
private AuthenticationHeaderValue _cachedHeader;
private Uri[] _authorizedUris;
private AccessTokenRequestOptions _tokenOptions;
/// <summary>
/// Initializes a new instance of <see cref="AuthorizationMessageHandler"/>.
/// </summary>
/// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for provisioning tokens.</param>
/// <param name="navigation">The <see cref="NavigationManager"/> to use for performing redirections.</param>
public GraphQlAuthorizationMessageHandler(
IAccessTokenProvider provider,
NavigationManager navigation,
IOptions<GraphQlAuthorizationMessageHandlerSettings> configuration)
{
_provider = provider;
// Invalidate the cached _lastToken when the authentication state changes
if (_provider is AuthenticationStateProvider authStateProvider)
{
_authenticationStateChangedHandler = _ => { _lastToken = null; };
authStateProvider.AuthenticationStateChanged += _authenticationStateChangedHandler;
}
ConfigureHandler(configuration.Value.AuthorizedUrls, configuration.Value.Scopes, configuration.Value.RedirectUri);
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var now = DateTimeOffset.Now;
if (_authorizedUris == null)
{
throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " +
$"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to.");
}
bool userAuthenticated = false;
// check if user is authenticated
if (_provider is AuthenticationStateProvider authStateProvider)
{
userAuthenticated = (await authStateProvider.GetAuthenticationStateAsync()).User.Identity?.IsAuthenticated == true;
}
if (userAuthenticated && _authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri)))
{
if (_lastToken == null || now >= _lastToken.Expires.AddMinutes(-5))
{
var tokenResult = _tokenOptions != null ?
await _provider.RequestAccessToken(_tokenOptions) :
await _provider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
_lastToken = token;
_cachedHeader = new AuthenticationHeaderValue("Bearer", _lastToken.Value);
}
else
{
Console.WriteLine("Failed to get token");
}
}
// We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request
// headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might
// not be able to provision a token without user interaction).
request.Headers.Authorization = _cachedHeader;
}
return await base.SendAsync(request, cancellationToken);
}
/// <summary>
/// Configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of
/// <paramref name="authorizedUrls" /> is a base of <see cref="HttpRequestMessage.RequestUri" />.
/// </summary>
/// <param name="authorizedUrls">The base addresses of endpoint URLs to which the token will be attached.</param>
/// <param name="scopes">The list of scopes to use when requesting an access token.</param>
/// <param name="returnUrl">The return URL to use in case there is an issue provisioning the token and a redirection to the
/// identity provider is necessary.
/// </param>
/// <returns>This <see cref="AuthorizationMessageHandler"/>.</returns>
public GraphQlAuthorizationMessageHandler ConfigureHandler(
IEnumerable<string> authorizedUrls,
IEnumerable<string> scopes = null,
string returnUrl = null)
{
if (_authorizedUris != null)
{
throw new InvalidOperationException("Handler already configured.");
}
if (authorizedUrls == null)
{
throw new ArgumentNullException(nameof(authorizedUrls));
}
var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray();
if (uris.Length == 0)
{
throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls));
}
_authorizedUris = uris;
var scopesList = scopes?.ToArray();
if (scopesList != null || returnUrl != null)
{
_tokenOptions = new AccessTokenRequestOptions
{
Scopes = scopesList,
ReturnUrl = returnUrl
};
}
return this;
}
void IDisposable.Dispose()
{
if (_provider is AuthenticationStateProvider authStateProvider)
{
authStateProvider.AuthenticationStateChanged -= _authenticationStateChangedHandler;
}
Dispose(disposing: true);
}
}
Now the ConfigureHandler method is called from within the constructor so it is needed also to Configure the new options like that:
builder.Services.Configure<GraphQlAuthorizationMessageHandlerSettings>(config =>
{
config.AuthorizedUrls = new [] { "http://localhost:5000/graphql" };
});