Skip to content
This repository has been archived by the owner on Nov 19, 2024. It is now read-only.

Commit

Permalink
Work in progress on fix for DPoP nonces received from the AS
Browse files Browse the repository at this point in the history
  • Loading branch information
josephdecock committed Apr 4, 2024
1 parent 4992b3b commit 786c033
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
Expand All @@ -22,6 +23,9 @@ public class ConfigureOpenIdConnectOptions : IConfigureNamedOptions<OpenIdConnec
private readonly IDPoPProofService _dPoPProofService;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOptions<UserTokenManagementOptions> _userAccessTokenManagementOptions;

private readonly ILoggerFactory _loggerFactory;

private readonly string? _configScheme;
private readonly string _clientName;

Expand All @@ -33,13 +37,14 @@ public ConfigureOpenIdConnectOptions(
IDPoPProofService dPoPProofService,
IHttpContextAccessor httpContextAccessor,
IOptions<UserTokenManagementOptions> userAccessTokenManagementOptions,
IAuthenticationSchemeProvider schemeProvider)
IAuthenticationSchemeProvider schemeProvider,
ILoggerFactory loggerFactory)
{
_dPoPNonceStore = dPoPNonceStore;
_dPoPProofService = dPoPProofService;
_httpContextAccessor = httpContextAccessor;
_userAccessTokenManagementOptions = userAccessTokenManagementOptions;

_configScheme = _userAccessTokenManagementOptions.Value.ChallengeScheme;
if (string.IsNullOrWhiteSpace(_configScheme))
{
Expand All @@ -55,6 +60,7 @@ public ConfigureOpenIdConnectOptions(
}

_clientName = OpenIdConnectTokenManagementDefaults.ClientCredentialsClientNamePrefix + _configScheme;
_loggerFactory = loggerFactory;
}

/// <inheritdoc/>
Expand All @@ -72,7 +78,9 @@ public void Configure(string? name, OpenIdConnectOptions options)
options.Events.OnAuthorizationCodeReceived = CreateCallback(options.Events.OnAuthorizationCodeReceived);
options.Events.OnTokenValidated = CreateCallback(options.Events.OnTokenValidated);

options.BackchannelHttpHandler = new DPoPProofTokenHandler(_dPoPProofService, _dPoPNonceStore, _httpContextAccessor)
var logger = _loggerFactory.CreateLogger<DPoPProofTokenHandler>();

options.BackchannelHttpHandler = new DPoPProofTokenHandler(_dPoPProofService, _dPoPNonceStore, _httpContextAccessor, logger)
{
InnerHandler = options.BackchannelHttpHandler ?? new HttpClientHandler()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,34 @@

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Duende.AccessTokenManagement.OpenIdConnect;

/// <summary>
/// Delegating handler that injects the DPoP proof token from the OIDC handler workflow
/// Delegating handler that injects the DPoP proof token. This is intended to be
/// used on the OIDC authentication handler's backchannel http client.
/// </summary>
class DPoPProofTokenHandler : DelegatingHandler
public class DPoPProofTokenHandler : DelegatingHandler
{
private readonly IDPoPProofService _dPoPProofService;
private readonly IDPoPNonceStore _dPoPNonceStore;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<DPoPProofTokenHandler> _logger;

/// <summary>
/// ctor
/// </summary>
/// <param name="dPoPProofService"></param>
/// <param name="dPoPNonceStore"></param>
/// <param name="httpContextAccessor"></param>
public DPoPProofTokenHandler(
internal DPoPProofTokenHandler(
IDPoPProofService dPoPProofService,
IDPoPNonceStore dPoPNonceStore,
IHttpContextAccessor httpContextAccessor)
IHttpContextAccessor httpContextAccessor,
ILogger<DPoPProofTokenHandler> logger)
{
_dPoPProofService = dPoPProofService;
_dPoPNonceStore = dPoPNonceStore;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}

/// <inheritdoc/>
Expand All @@ -40,23 +39,33 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
await SetDPoPProofTokenAsync(request, cancellationToken).ConfigureAwait(false);
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

// The authorization server might send us a new nonce in a successful
// request, indicating that we should use the new nonce in future.
var dPoPNonce = response.GetDPoPNonce();

// retry if 401
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && response.IsDPoPError())
if (dPoPNonce != null)
{
response.Dispose();
_logger.LogDebug("The authorization server has supplied a new nonce");

await SetDPoPProofTokenAsync(request, cancellationToken, dPoPNonce).ConfigureAwait(false);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
else if (dPoPNonce != null)
{
await _dPoPNonceStore.StoreNonceAsync(new DPoPNonceContext
{
Url = request.GetDPoPUrl(),
Method = request.Method.ToString(),
}, dPoPNonce);

// But the authorization server might also send a failure response, and expect us to retry
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{

// REVIEW: Is it good enough to check the status code and
// existence of the new nonce? Should we parse the response, and
// look for the "use_dpop_nonce" value in the error property?

_logger.LogDebug("Request failed (bad request). Retrying request with new DPoP proof token that includes the new nonce");
response.Dispose();
await SetDPoPProofTokenAsync(request, cancellationToken, dPoPNonce).ConfigureAwait(false);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}

return response;
Expand Down Expand Up @@ -85,8 +94,15 @@ protected virtual async Task SetDPoPProofTokenAsync(HttpRequestMessage request,

if (proofToken != null)
{
_logger.LogDebug("Sending DPoP proof token in request to endpoint: {url}",
request.RequestUri?.GetLeftPart(System.UriPartial.Path));
request.SetDPoPProofToken(proofToken.ProofToken);
}
else
{
_logger.LogDebug("No DPoP proof token in request to endpoint: {url}",
request.RequestUri?.GetLeftPart(System.UriPartial.Path));
}
}
}
}
2 changes: 1 addition & 1 deletion src/Duende.AccessTokenManagement/DPoPExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static void SetDPoPProofToken(this HttpRequestMessage request, string? pr
}

/// <summary>
/// Reads the WWW-Authenticate response header to determine if the respone is in error due to DPoP
/// Reads the WWW-Authenticate response header to determine if the response is in error due to DPoP
/// </summary>
public static bool IsDPoPError(this HttpResponseMessage response)
{
Expand Down

0 comments on commit 786c033

Please sign in to comment.