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

Work in progress on fix for DPoP nonces received from the AS #76

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading