diff --git a/src/Api/TestController.cs b/src/Api/TestController.cs index 4690d94..648f868 100644 --- a/src/Api/TestController.cs +++ b/src/Api/TestController.cs @@ -1,6 +1,6 @@ -using DPoPApi; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using IdentityModel; namespace Duende.IdentityServer.Demo { @@ -10,8 +10,8 @@ public class TestController : ControllerBase [Authorize(AuthenticationSchemes = IdentityServerConstants.LocalApi.AuthenticationScheme)] public IActionResult Get() { - var scheme = Request.GetAuthorizationScheme(); - var proofToken = Request.GetDPoPProofToken(); + var scheme = GetAuthorizationScheme(Request); + var proofToken = GetDPoPProofToken(Request); var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList(); @@ -28,8 +28,8 @@ public IActionResult Get() [Authorize(AuthenticationSchemes = "dpop")] public IActionResult GetDPoP() { - var scheme = Request.GetAuthorizationScheme(); - var proofToken = Request.GetDPoPProofToken(); + var scheme = GetAuthorizationScheme(Request); + var proofToken = GetDPoPProofToken(Request); var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList(); claims.Add(new { Type = "authorization_scheme", Value = scheme }); @@ -37,5 +37,15 @@ public IActionResult GetDPoP() return new JsonResult(claims); } + + private static string GetAuthorizationScheme(HttpRequest request) + { + return request.Headers.Authorization.FirstOrDefault()?.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0]; + } + + private static string GetDPoPProofToken(HttpRequest request) + { + return request.Headers[OidcConstants.HttpHeaders.DPoP].FirstOrDefault(); + } } } \ No newline at end of file diff --git a/src/DPoP/ConfigureJwtBearerOptions.cs b/src/DPoP/ConfigureJwtBearerOptions.cs deleted file mode 100644 index dd5c80c..0000000 --- a/src/DPoP/ConfigureJwtBearerOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; -using System; - -namespace DPoPApi; - -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/src/DPoP/DPoPExtensions.cs b/src/DPoP/DPoPExtensions.cs deleted file mode 100644 index 5aadfa4..0000000 --- a/src/DPoP/DPoPExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using IdentityModel; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; -using Microsoft.IdentityModel.Tokens; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace DPoPApi; - -/// -/// 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, 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-based cnf claim - /// - public static string CreateThumbprintCnf(this JsonWebKey jwk) - { - var jkt = jwk.CreateThumbprint(); - var values = new Dictionary - { - { JwtClaimTypes.ConfirmationMethods.JwkThumbprint, jkt } - }; - return JsonSerializer.Serialize(values); - } - - /// - /// Create the value of a thumbprint - /// - public static string CreateThumbprint(this JsonWebKey jwk) - { - var jkt = Base64Url.Encode(jwk.ComputeJwkThumbprint()); - return jkt; - } -} diff --git a/src/DPoP/DPoPJwtBearerEvents.cs b/src/DPoP/DPoPJwtBearerEvents.cs deleted file mode 100644 index 95bfb4c..0000000 --- a/src/DPoP/DPoPJwtBearerEvents.cs +++ /dev/null @@ -1,153 +0,0 @@ -using IdentityModel; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using static IdentityModel.OidcConstants; - -namespace DPoPApi; - -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(); - 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); - - // 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)) - { - 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/src/DPoP/DPoPMode.cs b/src/DPoP/DPoPMode.cs deleted file mode 100644 index c5d1292..0000000 --- a/src/DPoP/DPoPMode.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DPoPApi; - -public enum DPoPMode -{ - /// - /// Only DPoP tokens will be accepted - /// - DPoPOnly, - /// - /// Both DPoP and Bearer tokens will be accepted - /// - DPoPAndBearer -} diff --git a/src/DPoP/DPoPOptions.cs b/src/DPoP/DPoPOptions.cs deleted file mode 100644 index eaded7c..0000000 --- a/src/DPoP/DPoPOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace DPoPApi; - -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/src/DPoP/DPoPProofValidatonContext.cs b/src/DPoP/DPoPProofValidatonContext.cs deleted file mode 100644 index cbd2186..0000000 --- a/src/DPoP/DPoPProofValidatonContext.cs +++ /dev/null @@ -1,30 +0,0 @@ - -namespace DPoPApi; - -public class DPoPProofValidatonContext -{ - /// - /// The ASP.NET Core authentication scheme triggering the validation - /// - public string Scheme { get; set; } - - /// - /// The HTTP URL to validate - /// - public string Url { get; set; } - - /// - /// The HTTP method to validate - /// - public string Method { get; set; } - - /// - /// The DPoP proof token to validate - /// - public string ProofToken { get; set; } - - /// - /// The access token - /// - public string AccessToken { get; set; } -} diff --git a/src/DPoP/DPoPProofValidatonResult.cs b/src/DPoP/DPoPProofValidatonResult.cs deleted file mode 100644 index 05f0555..0000000 --- a/src/DPoP/DPoPProofValidatonResult.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; - -namespace DPoPApi; - -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/src/DPoP/DPoPProofValidator.cs b/src/DPoP/DPoPProofValidator.cs deleted file mode 100644 index 484a0a8..0000000 --- a/src/DPoP/DPoPProofValidator.cs +++ /dev/null @@ -1,485 +0,0 @@ -using IdentityModel; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace DPoPApi; - -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 with thumbprint: {jkt}", result.JsonWebKeyThumbprint); - result.IsError = false; - } - finally - { - if (result.IsError && String.IsNullOrWhiteSpace(result.Error)) - { - result.Error = OidcConstants.TokenErrors.InvalidDPoPProof; - } - } - - return result; - } - - /// - /// Validates the header. - /// - protected virtual Task ValidateHeaderAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result) - { - JsonWebToken token; - - try - { - var handler = new JsonWebTokenHandler(); - 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(JwtClaimTypes.TokenType, out var typ) || typ != JwtClaimTypes.JwtTypes.DPoPProofToken) - { - result.IsError = true; - result.ErrorDescription = "Invalid 'typ' value."; - return Task.CompletedTask; - } - - if (!token.TryGetHeaderValue(JwtClaimTypes.Algorithm, 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(); - result.Confirmation = jwk.CreateThumbprintCnf(); - - return Task.CompletedTask; - } - - /// - /// Validates the signature. - /// - protected virtual async Task ValidateSignatureAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result) - { - TokenValidationResult tokenValidationResult; - - 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."; - return; - } - - if (tokenValidationResult.Exception != null) - { - Logger.LogDebug("Error parsing DPoP token: {error}", tokenValidationResult.Exception.Message); - result.IsError = true; - result.ErrorDescription = "Invalid signature on DPoP token."; - return; - } - - result.Payload = tokenValidationResult.Claims; - } - - /// - /// Validates the payload. - /// - protected virtual async Task ValidatePayloadAsync(DPoPProofValidatonContext context, DPoPProofValidatonResult result) - { - 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)) - { - 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; - - Logger.LogDebug("Adding proof token with jti {jti} to replay cache for duration {cacheDuration}", result.TokenId, cacheDuration); - - 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)) - { - 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); - 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/src/DPoP/DPoPServiceCollectionExtensions.cs b/src/DPoP/DPoPServiceCollectionExtensions.cs deleted file mode 100644 index a0564da..0000000 --- a/src/DPoP/DPoPServiceCollectionExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System; - -namespace DPoPApi; - -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/src/DPoP/DefaultReplayCache.cs b/src/DPoP/DefaultReplayCache.cs deleted file mode 100644 index 69961fb..0000000 --- a/src/DPoP/DefaultReplayCache.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using System; -using System.Threading.Tasks; - -namespace DPoPApi; - -/// -/// 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/src/DPoP/IReplayCache.cs b/src/DPoP/IReplayCache.cs deleted file mode 100644 index 16c1838..0000000 --- a/src/DPoP/IReplayCache.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DPoPApi; - -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/src/Duende.IdentityServer.Demo.csproj b/src/Duende.IdentityServer.Demo.csproj index f0ba37c..4a576d0 100644 --- a/src/Duende.IdentityServer.Demo.csproj +++ b/src/Duende.IdentityServer.Demo.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/src/HostingExtensions.cs b/src/HostingExtensions.cs index bcd92db..74384ef 100644 --- a/src/HostingExtensions.cs +++ b/src/HostingExtensions.cs @@ -1,4 +1,4 @@ -using DPoPApi; +using Duende.AspNetCore.Authentication.JwtBearer.DPoP; using Duende.IdentityServer.Services; using Duende.IdentityServer.Validation; using IdentityServerHost; @@ -42,8 +42,8 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde .AddLocalApi() .AddJwtBearer("dpop", options => { - //options.Authority = "https://localhost:5001"; - options.Authority = "https://demo.duendesoftware.com"; + options.Authority = "https://localhost:5001"; + // options.Authority = "https://demo.duendesoftware.com"; options.TokenValidationParameters.ValidateAudience = false; options.MapInboundClaims = false; @@ -64,7 +64,7 @@ public static WebApplication ConfigureServices(this WebApplicationBuilder builde builder.Services.ConfigureDPoPTokensForScheme("dpop", options => { - options.Mode = DPoPMode.DPoPOnly; + options.TokenMode = DPoPMode.DPoPOnly; }); // add CORS policy for non-IdentityServer endpoints