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

Commit

Permalink
Merge pull request #79 from DuendeSoftware/joe/dpop-nonce-from-auth-s…
Browse files Browse the repository at this point in the history
…erver

Fix handling of dpop nonce sent during token exchange
  • Loading branch information
brockallen authored Apr 4, 2024
2 parents d8187f9 + ad95329 commit db3ac68
Show file tree
Hide file tree
Showing 13 changed files with 342 additions and 119 deletions.
2 changes: 2 additions & 0 deletions samples/Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

Log.Information("Host.Main Starting up");

Console.Title = "Web (Sample)";

try
{
var builder = WebApplication.CreateBuilder(args);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

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

namespace Duende.AccessTokenManagement.OpenIdConnect;

/// <summary>
/// Delegating handler that adds behavior needed for DPoP to the backchannel
/// http client of the OIDC authentication handler.
///
/// This handler has two main jobs:
///
/// 1. Store new nonces from successful responses from the authorization server.
///
/// 2. Attach proof tokens to token requests in the code flow.
///
/// On the authorize request, we will have sent a dpop_jkt parameter with a
/// key thumbprint. The AS expects that we will use the corresponding key to
/// create our proof, and we track that key in the http context. This handler
/// retrieves that key and uses it to create proof tokens for use in the code
/// flow.
///
/// Additionally, the token endpoint might respond to a token exchange
/// request with a request to retry with a nonce that it supplies via http
/// header. When it does, this handler retries those code exchange requests.
///
/// </summary>
internal class AuthorizationServerDPoPHandler : DelegatingHandler
{
private readonly IDPoPProofService _dPoPProofService;
private readonly IDPoPNonceStore _dPoPNonceStore;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<AuthorizationServerDPoPHandler> _logger;

internal AuthorizationServerDPoPHandler(
IDPoPProofService dPoPProofService,
IDPoPNonceStore dPoPNonceStore,
IHttpContextAccessor httpContextAccessor,
ILoggerFactory loggerFactory)
{
_dPoPProofService = dPoPProofService;
_dPoPNonceStore = dPoPNonceStore;
_httpContextAccessor = httpContextAccessor;
// We depend on the logger factory, rather than the logger itself, since
// the type parameter of the logger (referencing this class) will not
// always be accessible.
_logger = loggerFactory.CreateLogger<AuthorizationServerDPoPHandler>();
}

/// <inheritdoc/>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var codeExchangeJwk = _httpContextAccessor.HttpContext?.GetCodeExchangeDPoPKey();
if (codeExchangeJwk != null)
{
await SetDPoPProofTokenForCodeExchangeAsync(request, jwk: codeExchangeJwk).ConfigureAwait(false);
}

var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

// The authorization server might send us a new nonce on either a success or failure
var dPoPNonce = response.GetDPoPNonce();

if (dPoPNonce != null)
{
// This handler contains specialized logic to create the new proof
// token using the proof key that was associated with a code flow
// using a dpop_jkt parameter on the authorize call. Other flows
// (such as refresh), are separately responsible for retrying with a
// server-issued nonce. So, we ONLY do the retry logic when we have
// the dpop_jkt's jwk
if (codeExchangeJwk != null)
{
// If the http response code indicates a bad request, we can infer
// that we should retry with the new nonce.
//
// The server should have also set the error: use_dpop_nonce, but
// there's no need to incur the cost of parsing the json and
// checking for that, as we would only receive the nonce http header
// when that error was set. Authorization servers might preemptively
// send a new nonce, but the spec specifically says to do that on a
// success (and we handle that case in the else block).
//
// TL;DR - presence of nonce and 400 response code is enough to
// trigger a retry during code exchange
if (response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.LogDebug("Token request failed with DPoP nonce error. Retrying with new nonce.");
response.Dispose();
await SetDPoPProofTokenForCodeExchangeAsync(request, dPoPNonce, codeExchangeJwk).ConfigureAwait(false);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}

if (response.StatusCode == HttpStatusCode.OK)
{
_logger.LogDebug("The authorization server has supplied a new nonce on a successful response, which will be stored and used in future requests to the authorization server");

await _dPoPNonceStore.StoreNonceAsync(new DPoPNonceContext
{
Url = request.GetDPoPUrl(),
Method = request.Method.ToString(),
}, dPoPNonce);
}
}

return response;
}

/// <summary>
/// Creates a DPoP proof token and attaches it to a request.
/// </summary>
internal async Task SetDPoPProofTokenForCodeExchangeAsync(HttpRequestMessage request, string? dpopNonce = null, string? jwk = null)
{
if (!string.IsNullOrEmpty(jwk))
{
// remove any old headers
request.ClearDPoPProofToken();

// create proof
var proofToken = await _dPoPProofService.CreateProofTokenAsync(new DPoPProofRequest
{
Url = request.GetDPoPUrl(),
Method = request.Method.ToString(),
DPoPJsonWebKey = jwk,
DPoPNonce = dpopNonce,
});

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));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace Duende.AccessTokenManagement.OpenIdConnect;

/// <summary>
/// Named options to synthetize client credentials based on OIDC handler configuration
/// Named options to synthesize client credentials based on OIDC handler configuration
/// </summary>
public class ConfigureOpenIdConnectClientCredentialsOptions : IConfigureNamedOptions<ClientCredentialsClient>
{
Expand Down
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,7 @@ 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)
options.BackchannelHttpHandler = new AuthorizationServerDPoPHandler(_dPoPProofService, _dPoPNonceStore, _httpContextAccessor, _loggerFactory)
{
InnerHandler = options.BackchannelHttpHandler ?? new HttpClientHandler()
};
Expand Down Expand Up @@ -103,7 +109,10 @@ async Task Callback(RedirectContext context)
// checking for null allows for opt-out from using DPoP
if (jkt != null)
{
// we store the proof key here to associate it with the access token returned
// we store the proof key here to associate it with the
// authorization code that will be returned. Ultimately we
// use this to provide proof of possession during code
// exchange.
context.Properties.SetProofKey(key.JsonWebKey);

// pass jkt to authorize endpoint
Expand All @@ -126,7 +135,7 @@ Task Callback(AuthorizationCodeReceivedContext context)
if (jwk != null)
{
// set it so the OIDC message handler can find it
context.HttpContext.SetOutboundProofKey(jwk);
context.HttpContext.SetCodeExchangeDPoPKey(jwk);
}

return result;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Duende.AccessTokenManagement;
using Duende.AccessTokenManagement.OpenIdConnect;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Authentication;

Expand Down Expand Up @@ -105,11 +106,11 @@ internal static void RemoveProofKey(this AuthenticationProperties properties)
}

const string HttpContextDPoPKey = "dpop_proof_key";
internal static void SetOutboundProofKey(this HttpContext context, string key)
internal static void SetCodeExchangeDPoPKey(this HttpContext context, string key)
{
context.Items[HttpContextDPoPKey] = key;
}
internal static string? GetOutboundProofKey(this HttpContext context)
internal static string? GetCodeExchangeDPoPKey(this HttpContext context)
{
if (context.Items.ContainsKey(HttpContextDPoPKey))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public async Task<UserToken> RefreshAccessTokenAsync(
dPoPJsonWebKey != null &&
response.DPoPNonce != null)
{
_logger.LogDebug("DPoP error during token refresh. Retrying with server nonce");

var proof = await _dPoPProofService.CreateProofTokenAsync(new DPoPProofRequest
{
Url = request.Address!,
Expand Down
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
Loading

0 comments on commit db3ac68

Please sign in to comment.