diff --git a/IdentityServer/v7/DPoP/Api/Api.csproj b/IdentityServer/v7/DPoP/Api/Api.csproj
index 4969d1c2..a7051c50 100644
--- a/IdentityServer/v7/DPoP/Api/Api.csproj
+++ b/IdentityServer/v7/DPoP/Api/Api.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/ConfigureJwtBearerOptions.cs b/IdentityServer/v7/DPoP/Api/DPoP/ConfigureJwtBearerOptions.cs
deleted file mode 100644
index 53279060..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/ConfigureJwtBearerOptions.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.Extensions.Options;
-
-namespace Api;
-
-public class ConfigureJwtBearerOptions : IPostConfigureOptions
-{
- private readonly string _configScheme;
-
- public ConfigureJwtBearerOptions(string configScheme)
- {
- _configScheme = configScheme;
- }
-
- public void PostConfigure(string? name, JwtBearerOptions options)
- {
- if (_configScheme == name)
- {
- if (options.EventsType != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.EventsType))
- {
- throw new Exception("EventsType on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
- }
- if (options.Events != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.Events.GetType()))
- {
- throw new Exception("Events on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
- }
-
- if (options.Events == null && options.EventsType == null)
- {
- options.EventsType = typeof(DPoPJwtBearerEvents);
- }
- }
- }
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPExtensions.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPExtensions.cs
deleted file mode 100644
index b56c7bc1..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPExtensions.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using IdentityModel;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.IdentityModel.Tokens;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-
-namespace Api;
-
-///
-/// Extensions methods for DPoP
-///
-static class DPoPExtensions
-{
- const string DPoPPrefix = OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP + " ";
-
- public static bool IsDPoPAuthorizationScheme(this HttpRequest request)
- {
- var authz = request.Headers.Authorization.FirstOrDefault();
- return authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true;
- }
-
- public static bool TryGetDPoPAccessToken(this HttpRequest request, [NotNullWhen(true)]out string? token)
- {
- token = null;
-
- var authz = request.Headers.Authorization.FirstOrDefault();
- if (authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true)
- {
- token = authz[DPoPPrefix.Length..].Trim();
- return true;
- }
- return false;
- }
-
- public static string? GetAuthorizationScheme(this HttpRequest request)
- {
- return request.Headers.Authorization.FirstOrDefault()?.Split(' ', System.StringSplitOptions.RemoveEmptyEntries)[0];
- }
-
- public static string? GetDPoPProofToken(this HttpRequest request)
- {
- return request.Headers[OidcConstants.HttpHeaders.DPoP].FirstOrDefault();
- }
-
- public static string? GetDPoPNonce(this AuthenticationProperties props)
- {
- if (props.Items.ContainsKey("DPoP-Nonce"))
- {
- return props.Items["DPoP-Nonce"] as string;
- }
- return null;
- }
- public static void SetDPoPNonce(this AuthenticationProperties props, string nonce)
- {
- props.Items["DPoP-Nonce"] = nonce;
- }
-
- ///
- /// Create the value of a thumbprint
- ///
- public static string CreateThumbprint(this JsonWebKey jwk)
- {
- var jkt = Base64Url.Encode(jwk.ComputeJwkThumbprint());
- return jkt;
- }
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPJwtBearerEvents.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPJwtBearerEvents.cs
deleted file mode 100644
index a20af3ba..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPJwtBearerEvents.cs
+++ /dev/null
@@ -1,155 +0,0 @@
-using IdentityModel;
-using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.Extensions.Options;
-using Microsoft.Net.Http.Headers;
-using System.Text;
-using static IdentityModel.OidcConstants;
-
-namespace Api;
-
-public class DPoPJwtBearerEvents : JwtBearerEvents
-{
- private readonly IOptionsMonitor _optionsMonitor;
- private readonly DPoPProofValidator _validator;
-
- public DPoPJwtBearerEvents(IOptionsMonitor optionsMonitor, DPoPProofValidator validator)
- {
- _optionsMonitor = optionsMonitor;
- _validator = validator;
- }
-
- public override Task MessageReceived(MessageReceivedContext context)
- {
- var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);
-
- if (context.HttpContext.Request.TryGetDPoPAccessToken(out var token))
- {
- context.Token = token;
- }
- else if (dpopOptions.Mode == DPoPMode.DPoPOnly)
- {
- // this rejects the attempt for this handler,
- // since we don't want to attempt Bearer given the Mode
- context.NoResult();
- }
-
- return Task.CompletedTask;
- }
-
- public override async Task TokenValidated(TokenValidatedContext context)
- {
- var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);
-
- if (context.HttpContext.Request.TryGetDPoPAccessToken(out var at))
- {
- var proofToken = context.HttpContext.Request.GetDPoPProofToken();
- if (proofToken == null)
- {
- throw new InvalidOperationException("Missing DPoP (proof token) HTTP header");
- }
- var result = await _validator.ValidateAsync(new DPoPProofValidatonContext
- {
- Scheme = context.Scheme.Name,
- ProofToken = proofToken,
- AccessToken = at,
- Method = context.HttpContext.Request.Method,
- Url = context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + context.HttpContext.Request.PathBase + context.HttpContext.Request.Path
- });
-
- if (result.IsError)
- {
- // fails the result
- context.Fail(result.ErrorDescription ?? result.Error ?? throw new Exception("No ErrorDescription or Error set."));
-
- // we need to stash these values away so they are available later when the Challenge method is called later
- context.HttpContext.Items["DPoP-Error"] = result.Error;
- if (!string.IsNullOrWhiteSpace(result.ErrorDescription))
- {
- context.HttpContext.Items["DPoP-ErrorDescription"] = result.ErrorDescription;
- }
- if (!string.IsNullOrWhiteSpace(result.ServerIssuedNonce))
- {
- context.HttpContext.Items["DPoP-Nonce"] = result.ServerIssuedNonce;
- }
- }
- }
- else if (dpopOptions.Mode == DPoPMode.DPoPAndBearer)
- {
- // if the scheme used was not DPoP, then it was Bearer
- // and if a access token was presented with a cnf, then the
- // client should have sent it as DPoP, so we fail the request
- if (context.Principal?.HasClaim(x => x.Type == JwtClaimTypes.Confirmation) ?? false)
- {
- context.HttpContext.Items["Bearer-ErrorDescription"] = "Must use DPoP when using an access token with a 'cnf' claim";
- context.Fail("Must use DPoP when using an access token with a 'cnf' claim");
- }
- }
- }
-
- public override Task Challenge(JwtBearerChallengeContext context)
- {
- var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);
-
- if (dpopOptions.Mode == DPoPMode.DPoPOnly)
- {
- // if we are using DPoP only, then we don't need/want the default
- // JwtBearerHandler to add its WWW-Authenticate response header
- // so we have to set the status code ourselves
- context.Response.StatusCode = 401;
- context.HandleResponse();
- }
- else if (context.HttpContext.Items.ContainsKey("Bearer-ErrorDescription"))
- {
- var description = context.HttpContext.Items["Bearer-ErrorDescription"] as string;
- context.ErrorDescription = description;
- }
-
- if (context.HttpContext.Request.IsDPoPAuthorizationScheme())
- {
- // if we are challening due to dpop, then don't allow bearer www-auth to emit an error
- context.Error = null;
- }
-
- // now we always want to add our WWW-Authenticate for DPoP
- // For example:
- // WWW-Authenticate: DPoP error="invalid_dpop_proof", error_description="Invalid 'iat' value."
- var sb = new StringBuilder();
- sb.Append(OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP);
-
- if (context.HttpContext.Items.ContainsKey("DPoP-Error"))
- {
- var error = context.HttpContext.Items["DPoP-Error"] as string;
- sb.Append(" error=\"");
- sb.Append(error);
- sb.Append('\"');
-
- if (context.HttpContext.Items.ContainsKey("DPoP-ErrorDescription"))
- {
- var description = context.HttpContext.Items["DPoP-ErrorDescription"] as string;
-
- sb.Append(", error_description=\"");
- sb.Append(description);
- sb.Append('\"');
- }
- }
-
- context.Response.Headers.Append(HeaderNames.WWWAuthenticate, sb.ToString());
-
-
- if (context.HttpContext.Items.ContainsKey("DPoP-Nonce"))
- {
- var nonce = context.HttpContext.Items["DPoP-Nonce"] as string;
- context.Response.Headers[HttpHeaders.DPoPNonce] = nonce;
- }
- else
- {
- var nonce = context.Properties.GetDPoPNonce();
- if (nonce != null)
- {
- context.Response.Headers[HttpHeaders.DPoPNonce] = nonce;
- }
- }
-
- return Task.CompletedTask;
- }
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPMode.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPMode.cs
deleted file mode 100644
index 14e674fb..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPMode.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Api;
-
-public enum DPoPMode
-{
- ///
- /// Only DPoP tokens will be accepted
- ///
- DPoPOnly,
- ///
- /// Both DPoP and Bearer tokens will be accepted
- ///
- DPoPAndBearer
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPOptions.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPOptions.cs
deleted file mode 100644
index befffa72..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPOptions.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Api;
-
-public class DPoPOptions
-{
- public DPoPMode Mode { get; set; } = DPoPMode.DPoPOnly;
-
- public TimeSpan ProofTokenValidityDuration { get; set; } = TimeSpan.FromSeconds(1);
- public TimeSpan ClientClockSkew { get; set; } = TimeSpan.FromMinutes(0);
- public TimeSpan ServerClockSkew { get; set; } = TimeSpan.FromMinutes(5);
-
- public bool ValidateIat { get; set; } = true;
- public bool ValidateNonce { get; set; } = false;
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidatonContext.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidatonContext.cs
deleted file mode 100644
index a4ca897e..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidatonContext.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Security.Claims;
-
-namespace Api;
-
-public class DPoPProofValidatonContext
-{
- ///
- /// The ASP.NET Core authentication scheme triggering the validation
- ///
- public required string Scheme { get; set; }
-
- ///
- /// The HTTP URL to validate
- ///
- public required string Url { get; set; }
-
- ///
- /// The HTTP method to validate
- ///
- public required string Method { get; set; }
-
- ///
- /// The DPoP proof token to validate
- ///
- public required string ProofToken { get; set; }
-
- ///
- /// The access token
- ///
- public required string AccessToken { get; set; }
-
- ///
- /// The claims associated with the access token.
- ///
- public IEnumerable AccessTokenClaims { get; set; } = Enumerable.Empty();
-
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidatonResult.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidatonResult.cs
deleted file mode 100644
index 2c958a3a..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidatonResult.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-namespace Api;
-
-public class DPoPProofValidatonResult
-{
- public static DPoPProofValidatonResult Success = new DPoPProofValidatonResult { IsError = false };
-
- ///
- /// Indicates if the result was successful or not
- ///
- public bool IsError { get; set; }
-
- ///
- /// The error code for the validation result
- ///
- public string? Error { get; set; }
-
- ///
- /// The error description code for the validation result
- ///
- public string? ErrorDescription { get; set; }
-
- ///
- /// The serialized JWK from the validated DPoP proof token.
- ///
- public string? JsonWebKey { get; set; }
-
- ///
- /// The JWK thumbprint from the validated DPoP proof token.
- ///
- public string? JsonWebKeyThumbprint { get; set; }
-
- ///
- /// The cnf value for the DPoP proof token
- ///
- public string? Confirmation { get; set; }
-
- ///
- /// The payload value of the DPoP proof token.
- ///
- public IDictionary? Payload { get; internal set; }
-
- ///
- /// The jti value read from the payload.
- ///
- public string? TokenId { get; set; }
-
- ///
- /// The ath value read from the payload.
- ///
- public string? AccessTokenHash { get; set; }
-
- ///
- /// The nonce value read from the payload.
- ///
- public string? Nonce { get; set; }
-
- ///
- /// The iat value read from the payload.
- ///
- public long? IssuedAt { get; set; }
-
- ///
- /// The nonce value issued by the server.
- ///
- public string? ServerIssuedNonce { get; set; }
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidator.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidator.cs
deleted file mode 100644
index df0348a3..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPProofValidator.cs
+++ /dev/null
@@ -1,511 +0,0 @@
-using IdentityModel;
-using Microsoft.AspNetCore.DataProtection;
-using Microsoft.Extensions.Options;
-using Microsoft.IdentityModel.JsonWebTokens;
-using Microsoft.IdentityModel.Tokens;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-
-namespace Api;
-
-public class DPoPProofValidator
-{
- const string ReplayCachePurpose = "DPoPJwtBearerEvents-DPoPReplay-jti-";
- const string DataProtectorPurpose = "DPoPJwtBearerEvents-DPoPProofValidation-nonce";
-
- public readonly static IEnumerable SupportedDPoPSigningAlgorithms = new[]
- {
- SecurityAlgorithms.RsaSha256,
- SecurityAlgorithms.RsaSha384,
- SecurityAlgorithms.RsaSha512,
-
- SecurityAlgorithms.RsaSsaPssSha256,
- SecurityAlgorithms.RsaSsaPssSha384,
- SecurityAlgorithms.RsaSsaPssSha512,
-
- SecurityAlgorithms.EcdsaSha256,
- SecurityAlgorithms.EcdsaSha384,
- SecurityAlgorithms.EcdsaSha512
- };
-
- protected readonly IOptionsMonitor OptionsMonitor;
- protected readonly IDataProtector DataProtector;
- protected readonly IReplayCache ReplayCache;
- protected readonly ILogger Logger;
-
- public DPoPProofValidator(IOptionsMonitor optionsMonitor, IDataProtectionProvider dataProtectionProvider, IReplayCache replayCache, ILogger logger)
- {
- OptionsMonitor = optionsMonitor;
- DataProtector = dataProtectionProvider.CreateProtector(DataProtectorPurpose);
- ReplayCache = replayCache;
- Logger = logger;
- }
-
- ///
- public async Task ValidateAsync(DPoPProofValidatonContext context)
- {
- var result = new DPoPProofValidatonResult() { IsError = false };
-
- try
- {
- if (String.IsNullOrEmpty(context?.ProofToken))
- {
- result.IsError = true;
- result.ErrorDescription = "Missing DPoP proof value.";
- return result;
- }
-
- await ValidateHeaderAsync(context, result);
- if (result.IsError)
- {
- Logger.LogDebug("Failed to validate DPoP header");
- return result;
- }
-
- await ValidateSignatureAsync(context, result);
- if (result.IsError)
- {
- Logger.LogDebug("Failed to validate DPoP signature");
- return result;
- }
-
- await ValidatePayloadAsync(context, result);
- if (result.IsError)
- {
- Logger.LogDebug("Failed to validate DPoP payload");
- return result;
- }
-
- Logger.LogDebug("Successfully validated DPoP proof token");
- result.IsError = false;
- }
- finally
- {
- if (result.IsError)
- {
- result.Error = OidcConstants.TokenErrors.InvalidDPoPProof;
- }
- }
-
- return result;
- }
-
- ///
- /// Validates the header.
- ///
- protected virtual Task ValidateHeaderAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- JsonWebToken token;
- var handler = new JsonWebTokenHandler();
-
- try
- {
- token = handler.ReadJsonWebToken(context.ProofToken);
- }
- catch (Exception ex)
- {
- Logger.LogDebug("Error parsing DPoP token: {error}", ex.Message);
- result.IsError = true;
- result.ErrorDescription = "Malformed DPoP token.";
- return Task.CompletedTask;
- }
-
- if (!token.TryGetHeaderValue("typ", out var typ) || typ != JwtClaimTypes.JwtTypes.DPoPProofToken)
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'typ' value.";
- return Task.CompletedTask;
- }
-
- if (!token.TryGetHeaderValue("alg", out var alg) || !SupportedDPoPSigningAlgorithms.Contains(alg))
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'alg' value.";
- return Task.CompletedTask;
- }
-
- if (!token.TryGetHeaderValue(JwtClaimTypes.JsonWebKey, out var jwkValues))
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'jwk' value.";
- return Task.CompletedTask;
- }
-
- var jwkJson = JsonSerializer.Serialize(jwkValues);
-
- JsonWebKey jwk;
- try
- {
- jwk = new JsonWebKey(jwkJson);
- }
- catch (Exception ex)
- {
- Logger.LogDebug("Error parsing DPoP jwk value: {error}", ex.Message);
- result.IsError = true;
- result.ErrorDescription = "Invalid 'jwk' value.";
- return Task.CompletedTask;
- }
-
- if (jwk.HasPrivateKey)
- {
- result.IsError = true;
- result.ErrorDescription = "'jwk' value contains a private key.";
- return Task.CompletedTask;
- }
-
- result.JsonWebKey = jwkJson;
- result.JsonWebKeyThumbprint = jwk.CreateThumbprint();
-
- var accessToken = handler.ReadJsonWebToken(context.AccessToken);
- var cnf = accessToken.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Confirmation);
- if (cnf == null)
- {
- result.IsError = true;
- result.ErrorDescription = "Missing 'cnf' value.";
- return Task.CompletedTask;
- }
- var json = JsonSerializer.Deserialize>(cnf.Value);
- if (json == null)
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'cnf' value.";
- return Task.CompletedTask;
- }
- if (json.TryGetValue(JwtClaimTypes.ConfirmationMethods.JwkThumbprint, out var jktJson))
- {
- var accessTokenJkt = jktJson.ToString();
- if (accessTokenJkt != result.JsonWebKeyThumbprint)
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'cnf' value.";
- return Task.CompletedTask;
- }
- result.Confirmation = cnf.Value;
- }
-
- return Task.CompletedTask;
- }
-
- ///
- /// Validates the signature.
- ///
- protected virtual async Task ValidateSignatureAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- TokenValidationResult? tokenValidationResult = null;
-
- try
- {
- var key = new JsonWebKey(result.JsonWebKey);
- var tvp = new TokenValidationParameters
- {
- ValidateAudience = false,
- ValidateIssuer = false,
- ValidateLifetime = false,
- IssuerSigningKey = key,
- };
-
- var handler = new JsonWebTokenHandler();
- tokenValidationResult = await handler.ValidateTokenAsync(context.ProofToken, tvp);
- }
- catch (Exception ex)
- {
- Logger.LogDebug("Error parsing DPoP token: {error}", ex.Message);
- result.IsError = true;
- result.ErrorDescription = "Invalid signature on DPoP token.";
- }
-
- if (tokenValidationResult?.Exception != null)
- {
- Logger.LogDebug("Error parsing DPoP token: {error}", tokenValidationResult.Exception.Message);
- result.IsError = true;
- result.ErrorDescription = "Invalid signature on DPoP token.";
- }
-
- if (tokenValidationResult != null)
- {
- result.Payload = tokenValidationResult.Claims;
- }
- }
-
- ///
- /// Validates the payload.
- ///
- protected virtual async Task ValidatePayloadAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- if(result.Payload is null )
- {
- result.IsError = true;
- result.ErrorDescription = "Missing payload";
- return;
- }
-
- if (result.Payload.TryGetValue(JwtClaimTypes.DPoPAccessTokenHash, out var ath))
- {
- result.AccessTokenHash = ath as string;
- }
-
- if (String.IsNullOrEmpty(result.AccessTokenHash))
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'ath' value.";
- return;
- }
-
- using (var sha = SHA256.Create())
- {
- var bytes = Encoding.UTF8.GetBytes(context.AccessToken);
- var hash = sha.ComputeHash(bytes);
-
- var accessTokenHash = Base64Url.Encode(hash);
- if (accessTokenHash != result.AccessTokenHash)
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'ath' value.";
- return;
- }
- }
-
- if (result.Payload.TryGetValue(JwtClaimTypes.JwtId, out var jti))
- {
- result.TokenId = jti as string;
- }
-
- if (String.IsNullOrEmpty(result.TokenId))
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'jti' value.";
- return;
- }
-
- if (!result.Payload.TryGetValue(JwtClaimTypes.DPoPHttpMethod, out var htm) || !context.Method.Equals(htm))
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'htm' value.";
- return;
- }
-
- if (!result.Payload.TryGetValue(JwtClaimTypes.DPoPHttpUrl, out var htu) || !context.Url.Equals(htu))
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'htu' value.";
- return;
- }
-
- if (result.Payload.TryGetValue(JwtClaimTypes.IssuedAt, out var iat))
- {
- if (iat is int)
- {
- result.IssuedAt = (int) iat;
- }
- if (iat is long)
- {
- result.IssuedAt = (long) iat;
- }
- }
-
- if (!result.IssuedAt.HasValue)
- {
- result.IsError = true;
- result.ErrorDescription = "Missing 'iat' value.";
- return;
- }
-
- if (result.Payload.TryGetValue(JwtClaimTypes.Nonce, out var nonce))
- {
- result.Nonce = nonce as string;
- }
-
- await ValidateFreshnessAsync(context, result);
- if (result.IsError)
- {
- Logger.LogDebug("Failed to validate DPoP token freshness");
- return;
- }
-
- // we do replay at the end so we only add to the reply cache if everything else is ok
- await ValidateReplayAsync(context, result);
- if (result.IsError)
- {
- Logger.LogDebug("Detected replay of DPoP token");
- return;
- }
- }
-
- ///
- /// Validates is the token has been replayed.
- ///
- protected virtual async Task ValidateReplayAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- var dpopOptions = OptionsMonitor.Get(context.Scheme);
-
- if (await ReplayCache.ExistsAsync(ReplayCachePurpose, result.TokenId!)) // jti is required by an earlier validation
- {
- result.IsError = true;
- result.ErrorDescription = "Detected DPoP proof token replay.";
- return;
- }
-
- // get largest skew based on how client's freshness is validated
- var validateIat = dpopOptions.ValidateIat;
- var validateNonce = dpopOptions.ValidateNonce;
- var skew = TimeSpan.Zero;
- if (validateIat && dpopOptions.ClientClockSkew > skew)
- {
- skew = dpopOptions.ClientClockSkew;
- }
- if (validateNonce && dpopOptions.ServerClockSkew > skew)
- {
- skew = dpopOptions.ServerClockSkew;
- }
-
- // we do x2 here because clock might be might be before or after, so we're making cache duration
- // longer than the likelyhood of proof token expiration, which is done before replay
- skew *= 2;
- var cacheDuration = dpopOptions.ProofTokenValidityDuration + skew;
- await ReplayCache.AddAsync(ReplayCachePurpose, result.TokenId!, DateTimeOffset.UtcNow.Add(cacheDuration));
- }
-
- ///
- /// Validates the freshness.
- ///
- protected virtual async Task ValidateFreshnessAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- var dpopOptions = OptionsMonitor.Get(context.Scheme);
-
- var validateIat = dpopOptions.ValidateIat;
- if (validateIat)
- {
- await ValidateIatAsync(context, result);
- if (result.IsError)
- {
- return;
- }
- }
-
- var validateNonce = dpopOptions.ValidateNonce;
- if (validateNonce)
- {
- await ValidateNonceAsync(context, result);
- if (result.IsError)
- {
- return;
- }
- }
- }
-
- ///
- /// Validates the freshness of the iat value.
- ///
- protected virtual Task ValidateIatAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- var dpopOptions = OptionsMonitor.Get(context.Scheme);
-
- if (IsExpired(context, result, dpopOptions.ClientClockSkew, result.IssuedAt!.Value)) // iat is required by an earlier validation
- {
- result.IsError = true;
- result.ErrorDescription = "Invalid 'iat' value.";
- return Task.CompletedTask;
- }
-
- return Task.CompletedTask;
- }
-
- ///
- /// Validates the freshness of the nonce value.
- ///
- protected virtual async Task ValidateNonceAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- if (String.IsNullOrWhiteSpace(result.Nonce))
- {
- result.IsError = true;
- result.Error = OidcConstants.TokenErrors.UseDPoPNonce;
- result.ErrorDescription = "Missing 'nonce' value.";
- result.ServerIssuedNonce = CreateNonce(context, result);
- return;
- }
-
- var time = await GetUnixTimeFromNonceAsync(context, result);
- if (time <= 0)
- {
- Logger.LogDebug("Invalid time value read from the 'nonce' value");
-
- result.IsError = true;
- result.ErrorDescription = "Invalid 'nonce' value.";
- result.ServerIssuedNonce = CreateNonce(context, result);
- return;
- }
-
- var dpopOptions = OptionsMonitor.Get(context.Scheme);
-
- if (IsExpired(context, result, dpopOptions.ServerClockSkew, time))
- {
- Logger.LogDebug("DPoP 'nonce' expiration failed. It's possible that the server farm clocks might not be closely synchronized, so consider setting the ServerClockSkew on the DPoPOptions on the IdentityServerOptions.");
-
- result.IsError = true;
- result.ErrorDescription = "Invalid 'nonce' value.";
- result.ServerIssuedNonce = CreateNonce(context, result);
- return;
- }
- }
-
- ///
- /// Creates a nonce value to return to the client.
- ///
- ///
- protected virtual string CreateNonce(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- return DataProtector.Protect(now.ToString());
- }
-
- ///
- /// Reads the time the nonce was created.
- ///
- ///
- protected virtual ValueTask GetUnixTimeFromNonceAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result)
- {
- try
- {
- var value = DataProtector.Unprotect(result.Nonce!); // nonce is required by an earlier validation
- if (Int64.TryParse(value, out long iat))
- {
- return ValueTask.FromResult(iat);
- }
- }
- catch (Exception ex)
- {
- Logger.LogDebug("Error parsing DPoP 'nonce' value: {error}", ex.ToString());
- }
-
- return ValueTask.FromResult(0);
- }
-
- ///
- /// Validates the expiration of the DPoP proof.
- /// Returns true if the time is beyond the allowed limits, false otherwise.
- ///
- protected virtual bool IsExpired(DPoPProofValidatonContext context, DPoPProofValidatonResult result, TimeSpan clockSkew, long issuedAtTime)
- {
- var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- var start = now + (int) clockSkew.TotalSeconds;
- if (start < issuedAtTime)
- {
- var diff = issuedAtTime - now;
- Logger.LogDebug("Expiration check failed. Creation time was too far in the future. The time being checked was {iat}, and clock is now {now}. The time difference is {diff}", issuedAtTime, now, diff);
- return true;
- }
-
- var dpopOptions = OptionsMonitor.Get(context.Scheme);
- var expiration = issuedAtTime + (int) dpopOptions.ProofTokenValidityDuration.TotalSeconds;
- var end = now - (int) clockSkew.TotalSeconds;
- if (expiration < end)
- {
- var diff = now - expiration;
- Logger.LogDebug("Expiration check failed. Expiration has already happened. The expiration was at {exp}, and clock is now {now}. The time difference is {diff}", expiration, now, diff);
- return true;
- }
-
- return false;
- }
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DPoPServiceCollectionExtensions.cs b/IdentityServer/v7/DPoP/Api/DPoP/DPoPServiceCollectionExtensions.cs
deleted file mode 100644
index 300a8361..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DPoPServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.Extensions.Options;
-
-namespace Api;
-
-static class DPoPServiceCollectionExtensions
-{
- public static IServiceCollection ConfigureDPoPTokensForScheme(this IServiceCollection services, string scheme)
- {
- services.AddOptions();
-
- services.AddTransient();
- services.AddTransient();
- services.AddDistributedMemoryCache();
- services.AddTransient();
-
- services.AddSingleton>(new ConfigureJwtBearerOptions(scheme));
-
-
- return services;
- }
-
- public static IServiceCollection ConfigureDPoPTokensForScheme(this IServiceCollection services, string scheme, Action configure)
- {
- services.Configure(scheme, configure);
- return services.ConfigureDPoPTokensForScheme(scheme);
- }
-}
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/DefaultReplayCache.cs b/IdentityServer/v7/DPoP/Api/DPoP/DefaultReplayCache.cs
deleted file mode 100644
index 93128ec8..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/DefaultReplayCache.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Microsoft.Extensions.Caching.Distributed;
-
-namespace Api;
-
-///
-/// Default implementation of the replay cache using IDistributedCache
-///
-public class DefaultReplayCache : IReplayCache
-{
- private const string Prefix = nameof(DefaultReplayCache) + "-";
-
- private readonly IDistributedCache _cache;
-
- ///
- /// ctor
- ///
- ///
- public DefaultReplayCache(IDistributedCache cache)
- {
- _cache = cache;
- }
-
- ///
- public async Task AddAsync(string purpose, string handle, DateTimeOffset expiration)
- {
- var options = new DistributedCacheEntryOptions
- {
- AbsoluteExpiration = expiration
- };
-
- await _cache.SetAsync(Prefix + purpose + handle, new byte[] { }, options);
- }
-
- ///
- public async Task ExistsAsync(string purpose, string handle)
- {
- return (await _cache.GetAsync(Prefix + purpose + handle, default)) != null;
- }
-}
\ No newline at end of file
diff --git a/IdentityServer/v7/DPoP/Api/DPoP/IReplayCache.cs b/IdentityServer/v7/DPoP/Api/DPoP/IReplayCache.cs
deleted file mode 100644
index c08018e6..00000000
--- a/IdentityServer/v7/DPoP/Api/DPoP/IReplayCache.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace Api;
-
-public interface IReplayCache
-{
- ///
- /// Adds a handle to the cache
- ///
- ///
- ///
- ///
- ///
- Task AddAsync(string purpose, string handle, DateTimeOffset expiration);
-
-
- ///
- /// Checks if a cached handle exists
- ///
- ///
- ///
- ///
- Task ExistsAsync(string purpose, string handle);
-}
diff --git a/IdentityServer/v7/DPoP/Api/IdentityController.cs b/IdentityServer/v7/DPoP/Api/IdentityController.cs
index 770905cf..5fb22660 100644
--- a/IdentityServer/v7/DPoP/Api/IdentityController.cs
+++ b/IdentityServer/v7/DPoP/Api/IdentityController.cs
@@ -1,5 +1,4 @@
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authorization;
+using IdentityModel;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers;
@@ -20,20 +19,19 @@ public ActionResult Get()
var claims = User.Claims.Select(c => new { c.Type, c.Value });
_logger.LogInformation("claims: {claims}", claims);
- var scheme = Request.GetAuthorizationScheme();
- var proofToken = Request.GetDPoPProofToken();
+ var scheme = GetAuthorizationScheme(Request);
+ var proofToken = GetDPoPProofToken(Request);
return new JsonResult(new { scheme, proofToken, claims });
}
-
- [HttpGet("TestNonce")]
- [AllowAnonymous]
- public ActionResult TestNonce()
+
+ public static string? GetAuthorizationScheme(HttpRequest request)
{
- var x = Request.GetDPoPProofToken();
- var props = new AuthenticationProperties();
- props.SetDPoPNonce("custom-nonce");
+ return request.Headers.Authorization.FirstOrDefault()?.Split(' ', System.StringSplitOptions.RemoveEmptyEntries)[0];
+ }
- return Challenge(props);
+ public static string? GetDPoPProofToken(HttpRequest request)
+ {
+ return request.Headers[OidcConstants.HttpHeaders.DPoP].FirstOrDefault();
}
}
\ No newline at end of file
diff --git a/IdentityServer/v7/DPoP/Api/Program.cs b/IdentityServer/v7/DPoP/Api/Program.cs
index 7ffde596..a60c3abe 100644
--- a/IdentityServer/v7/DPoP/Api/Program.cs
+++ b/IdentityServer/v7/DPoP/Api/Program.cs
@@ -1,4 +1,4 @@
-using Api;
+using Duende.AspNetCore.Authentication.JwtBearer.DPoP;
using Serilog;
using Serilog.Sinks.SystemConsole.Themes;
@@ -24,11 +24,23 @@
options.TokenValidationParameters.ValidateAudience = false;
options.MapInboundClaims = false;
- options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
+ options.TokenValidationParameters.ValidTypes = ["at+jwt"];
});
// layers DPoP onto the "token" scheme above
-builder.Services.ConfigureDPoPTokensForScheme("token");
+builder.Services.ConfigureDPoPTokensForScheme("token", opt =>
+{
+ // Chose a validation mode: either Nonce or IssuedAt. With nonce validation,
+ // the api supplies a nonce that must be used to prove that the token was
+ // not pre-generated. With IssuedAt validation, the client includes the
+ // current time in the proof token, which is compared to the clock. Nonce
+ // validation provides protection against some attacks that are possible
+ // with IssuedAt validation, at the cost of an additional HTTP request being
+ // required each time the API is invoked.
+ //
+ // See RFC 9449 for more details.
+ opt.ValidationMode = ExpirationValidationMode.IssuedAt; // IssuedAt is the default.
+});
var app = builder.Build();