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("""