diff --git a/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt b/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt index d43e0516b..feb67e12d 100644 --- a/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt @@ -4,6 +4,13 @@ Availability: .NET 9 and .NET 8 # ALM - CHANGED Dependencies to latest and greatest with respect to TFMs   +# Bug Fixes +- FIXED DigestAuthenticationHandler class in the Cuemon.AspNetCore.Authentication.Digest namespace to remove quoted string values for the following parameters: stale and algorithm +- FIXED DigestAuthenticationMiddleware class in the Cuemon.AspNetCore.Authentication.Digest namespace to remove quoted string values for the following parameters: stale and algorithm +- FIXED DigestAuthorizationHeader class in the Cuemon.AspNetCore.Authentication.Digest namespace to remove quoted string values for the following parameters: algorithm, qop and nc +- FIXED DigestAuthorizationHeader class in the Cuemon.AspNetCore.Authentication.Digest namespace so stale is removed from the parameters including marking previous code obsolete +- FIXED DigestAuthorizationHeader class in the Cuemon.AspNetCore.Authentication.Digest namespace to accommodate the issues mentioned in https://github.com/gimlichael/Cuemon/issues/115 +  Version 9.0.3 Availability: .NET 9 and .NET 8   diff --git a/CHANGELOG.md b/CHANGELOG.md index 9accb0bd0..fe05efe56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. -## [9.0.4] - 2025-04-09 +## [9.0.4] - 2025-04-10 -This is a service update that focuses on package dependencies. +This is a service update that focuses on package dependencies and a few bug fixes. + +> [!WARNING] +> The fix applied to the `DigestAuthenticationHandler`, `DigestAuthenticationMiddleware`, and `DigestAuthorizationHeader` classes in the `Cuemon.AspNetCore.Authentication.Digest` namespace changes both the `WWW-Authenticate` and `Authorization` headers. Justification for this patch is mentioned in [GitHub Issue #115](https://github.com/gimlichael/Cuemon/issues/115), but may affect existing implementations that rely on the previous behavior. + +### Fixed + +- Updated the `DigestAuthenticationHandler` class in the `Cuemon.AspNetCore.Authentication.Digest` namespace to remove quoted string values for the `stale` and `algorithm` parameters, +- Updated the `DigestAuthenticationMiddleware` class in the `Cuemon.AspNetCore.Authentication.Digest` namespace to remove quoted string values for the `stale` and `algorithm` parameters, +- Updated the `DigestAuthorizationHeader` class in the `Cuemon.AspNetCore.Authentication.Digest` namespace to: + - Remove quoted string values for the `algorithm`, `qop`, and `nc` parameters, + - Exclude the `stale` parameter and mark the previous implementation as obsolete, + - Address issues outlined in [GitHub Issue #115](https://github.com/gimlichael/Cuemon/issues/115). ## [9.0.3] - 2025-03-31 diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs index 7fb424da4..bc63cd1c8 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs @@ -66,7 +66,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop var nonceGenerator = Options.NonceGenerator; var staleNonce = Context.Items[DigestFields.Stale] as string ?? "false"; AuthenticationHandlerFeature.Set(await HandleAuthenticateOnceSafeAsync().ConfigureAwait(false), Context); // so annoying that Microsoft does not propagate AuthenticateResult properly - other have noticed as well: https://github.com/dotnet/aspnetcore/issues/44100 - Decorator.Enclose(Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale=\"{staleNonce}\", algorithm=\"{DigestAuthenticationMiddleware.ParseAlgorithm(Options.Algorithm)}\"")); + Decorator.Enclose(Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale={staleNonce}, algorithm={DigestAuthenticationMiddleware.ParseAlgorithm(Options.Algorithm)}")); await base.HandleChallengeAsync(properties).ConfigureAwait(false); } } diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs index 996387f53..3ee46e678 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs @@ -61,7 +61,7 @@ await Decorator.Enclose(context).InvokeUnauthorizedExceptionAsync(Options, princ var nonceSecret = Options.NonceSecret; var nonceGenerator = Options.NonceGenerator; var staleNonce = dc.Items[DigestFields.Stale] as string ?? "false"; - Decorator.Enclose(dc.Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale=\"{staleNonce}\", algorithm=\"{ParseAlgorithm(Options.Algorithm)}\"")); + Decorator.Enclose(dc.Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale={staleNonce}, algorithm={ParseAlgorithm(Options.Algorithm)}")); }).ConfigureAwait(false); } diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeader.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeader.cs index b5a1f5815..83641f333 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeader.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeader.cs @@ -27,7 +27,7 @@ public class DigestAuthorizationHeader : AuthorizationHeader public static DigestAuthorizationHeader Create(string authorizationHeader) { Validator.ThrowIfNullOrWhitespace(authorizationHeader); - return new DigestAuthorizationHeader().Parse(authorizationHeader, o => o.CredentialsDelimiter = " ") as DigestAuthorizationHeader; + return new DigestAuthorizationHeader().Parse(authorizationHeader, o => o.CredentialsDelimiter = ", ") as DigestAuthorizationHeader; } /// @@ -39,6 +39,33 @@ private DigestAuthorizationHeader() : base(Scheme) { } + /// + /// Initializes a new instance of the class. + /// + /// The realm/credential scope that defines the remote resource. + /// The unique server generated string. + /// The string of data specified by the server. + /// The algorithm used to produce the digest and an unkeyed digest. + /// The username of the specified . + /// The effective request URI. + /// The hexadecimal count of the number of requests the client has sent with the value. + /// The unique client generated string. + /// The "quality of protection" the client has applied to the message. + /// The computed response which proves that the user knows a password. + public DigestAuthorizationHeader(string realm, string nonce, string opaque, string algorithm, string userName, string uri, string nc, string cNonce, string qop, string response) : base(Scheme) + { + Realm = realm; + Nonce = nonce; + Opaque = opaque; + Algorithm = algorithm; + UserName = userName; + Uri = uri; + NC = nc; + CNonce = cNonce; + Qop = qop; + Response = response; + } + /// /// Initializes a new instance of the class. /// @@ -53,6 +80,7 @@ private DigestAuthorizationHeader() : base(Scheme) /// The unique client generated string. /// The "quality of protection" the client has applied to the message. /// The computed response which proves that the user knows a password. + [Obsolete("This constructor is obsolete and will be removed in a future version. Use the 'DigestAuthorizationHeader' constructor without the 'stale' parameter instead.")] public DigestAuthorizationHeader(string realm, string nonce, string opaque, string stale, string algorithm, string userName, string uri, string nc, string cNonce, string qop, string response) : base(Scheme) { Realm = realm; @@ -132,6 +160,7 @@ public DigestAuthorizationHeader(string realm, string nonce, string opaque, stri /// Gets the case-insensitive flag indicating if the previous request from the client was rejected because the value was stale. /// /// The case-insensitive flag indicating if the previous request from the client was rejected because the value was stale. + [Obsolete("This property is obsolete and will be removed in a future version.")] public string Stale { get; } /// @@ -151,8 +180,7 @@ protected override AuthorizationHeader ParseCore(IReadOnlyDictionary @@ -166,19 +194,25 @@ public override string ToString() AppendField(sb, DigestFields.Realm, Realm); AppendField(sb, DigestFields.Nonce, Nonce); AppendField(sb, DigestFields.DigestUri, Uri); - AppendField(sb, DigestFields.QualityOfProtection, Qop); - AppendField(sb, DigestFields.NonceCount, NC); + AppendField(sb, DigestFields.QualityOfProtection, Qop, false); + AppendField(sb, DigestFields.NonceCount, NC, false); AppendField(sb, DigestFields.ClientNonce, CNonce); AppendField(sb, DigestFields.Response, Response); AppendField(sb, DigestFields.Opaque, Opaque); - AppendField(sb, DigestFields.Stale, Stale); - AppendField(sb, DigestFields.Algorithm, Algorithm); - return sb.ToString(); + AppendField(sb, DigestFields.Algorithm, Algorithm, false); + return sb.ToString().TrimEnd(','); + } + + private static void AppendField(StringBuilder sb, string fn, string fv, bool useQuotedStringSyntax = true) + { + if (!string.IsNullOrWhiteSpace(fv)) { sb.Append(CultureInfo.InvariantCulture, $" {fn}={Parse(fv, useQuotedStringSyntax)},"); } } - private static void AppendField(StringBuilder sb, string fn, string fv) + private static string Parse(string value, bool useQuotedStringSyntax) { - if (!string.IsNullOrWhiteSpace(fv)) { sb.Append(CultureInfo.InvariantCulture, $" {fn}=\"{fv}\""); } + return useQuotedStringSyntax + ? $"\"{value}\"" + : value; } } } diff --git a/src/Cuemon.AspNetCore.Authentication/GlobalSuppressions.cs b/src/Cuemon.AspNetCore.Authentication/GlobalSuppressions.cs index 2d848fce9..5b3c9dc87 100644 --- a/src/Cuemon.AspNetCore.Authentication/GlobalSuppressions.cs +++ b/src/Cuemon.AspNetCore.Authentication/GlobalSuppressions.cs @@ -8,3 +8,4 @@ [assembly: SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "By design to support the Digest protocol.", Scope = "member", Target = "~M:Cuemon.AspNetCore.Authentication.Digest.DigestAuthorizationHeader.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String)")] [assembly: SuppressMessage("Performance", "CA1847:Use char literal for a single character lookup", Justification = "Not supported in .NET Standard 2.0 (and not an issue with an extra pico-second).", Scope = "member", Target = "~M:Cuemon.AspNetCore.Authentication.Basic.BasicAuthorizationHeader.#ctor(System.String,System.String)")] [assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Intentional as these embark on IDecorator.", Scope = "namespace", Target = "~N:Cuemon.AspNetCore.Authentication")] +[assembly: SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "By design to support the Digest protocol.", Scope = "member", Target = "~M:Cuemon.AspNetCore.Authentication.Digest.DigestAuthorizationHeader.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String,System.String)")] diff --git a/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs b/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs index 87082ce8e..619fbb2f3 100644 --- a/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs +++ b/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs @@ -651,7 +651,7 @@ public async void AuthorizationResponseHandler_DigestScheme_ShouldRenderResponse TestOutput.WriteLine(content); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); - Assert.Equal("Digest realm=\"AuthenticationServer\", qop=\"auth, auth-int\", nonce=\"MjAyNC0wMi0wMyAyMTo1NjoyMVo6MDlhZTFhZDIyZGE4ZGExYTAxMmVkMzMwZWJlMzVkOTNlOGNmYTFmN2FiMzU5YzY0YTUwODFjZThkYjM1NzIwZA==\", opaque=\"dd1867244f862b1f858784a9b276d609\", stale=\"false\", algorithm=\"SHA-256\"", result.Headers.WwwAuthenticate.ToString()); + Assert.Equal("Digest realm=\"AuthenticationServer\", qop=\"auth, auth-int\", nonce=\"MjAyNC0wMi0wMyAyMTo1NjoyMVo6MDlhZTFhZDIyZGE4ZGExYTAxMmVkMzMwZWJlMzVkOTNlOGNmYTFmN2FiMzU5YzY0YTUwODFjZThkYjM1NzIwZA==\", opaque=\"dd1867244f862b1f858784a9b276d609\", stale=false, algorithm=SHA-256", result.Headers.WwwAuthenticate.ToString()); if (sensitivityDetails == FaultSensitivityDetails.All) { Assert.Equal("""