File: Internal\AccessTokenHttpMessageHandler.cs
Web Access
Project: src\aspnetcore\src\SignalR\clients\csharp\Http.Connections.Client\src\Microsoft.AspNetCore.Http.Connections.Client.csproj (Microsoft.AspNetCore.Http.Connections.Client)
// 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;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
 
namespace Microsoft.AspNetCore.Http.Connections.Client.Internal;
 
internal sealed class AccessTokenHttpMessageHandler : DelegatingHandler
{
    private readonly HttpConnection _httpConnection;
    private string? _accessToken;
 
    public AccessTokenHttpMessageHandler(HttpMessageHandler inner, HttpConnection httpConnection) : base(inner)
    {
        _httpConnection = httpConnection;
    }
 
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
#if NET5_0_OR_GREATER
        var isNegotiate = request.Options.TryGetValue(new HttpRequestOptionsKey<bool>("IsNegotiate"), out var negotiateValue) && negotiateValue;
        var isRefresh = request.Options.TryGetValue(new HttpRequestOptionsKey<bool>("IsRefresh"), out var refreshValue) && refreshValue;
#else
        var isNegotiate = request.Properties.TryGetValue("IsNegotiate", out var negotiateValue) && negotiateValue is true;
        var isRefresh = request.Properties.TryGetValue("IsRefresh", out var refreshValue) && refreshValue is true;
#endif
 
        var shouldRetry = true;
        // The token to attach to this request. Defaults to the cached token for normal transport
        // requests (polls/sends), which must keep using whatever the cache currently holds.
        var tokenForRequest = _accessToken;
        if (string.IsNullOrEmpty(_accessToken) || isNegotiate || isRefresh)
        {
            shouldRetry = false;
            // Negotiate redirects likely will have a new access token so let's always grab a (potentially) new access token on negotiate.
            // Authentication refresh exists specifically to obtain a new access token, so always re-fetch on refresh too.
            tokenForRequest = await _httpConnection.GetAccessTokenAsync().ConfigureAwait(false);
 
            // For negotiate (and the initial fetch) adopt the new token immediately. For refresh, defer
            // updating the cache until we know the server accepted the refresh (below): a rejected
            // refresh (for example an OnAuthenticationRefresh denial returning 403) must not poison the cache and
            // leak the rejected token onto subsequent transport requests (e.g. Long Polling polls/sends).
            if (!isRefresh)
            {
                _accessToken = tokenForRequest;
            }
        }
 
        SetAccessToken(tokenForRequest, request);
 
        var result = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
 
        if (isRefresh)
        {
            // Only adopt the refreshed token once the server has accepted the refresh, so subsequent
            // transport requests use the new token. On rejection, keep the previously cached token.
            if (result.IsSuccessStatusCode)
            {
                _accessToken = tokenForRequest;
            }
        }
        // retry once with a new token on auth failure
        else if (shouldRetry && result.StatusCode is HttpStatusCode.Unauthorized)
        {
            HttpConnection.Log.RetryAccessToken(_httpConnection._logger, result.StatusCode);
            result.Dispose();
            _accessToken = await _httpConnection.GetAccessTokenAsync().ConfigureAwait(false);
 
            SetAccessToken(_accessToken, request);
 
            // Retrying the request relies on any HttpContent being non-disposable.
            // Currently this is true, the only HttpContent we send is type ReadOnlySequenceContent which is used by SSE and LongPolling for sending an already buffered byte[]
            result = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
        return result;
    }
 
    private static void SetAccessToken(string? accessToken, HttpRequestMessage request)
    {
        if (!string.IsNullOrEmpty(accessToken))
        {
            // Don't need to worry about WebSockets and browser because this code path will not be hit in the browser case
            // ClientWebSocketOptions.HttpVersion isn't settable in the browser
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        }
    }
}