Skip to content

Commit 57bc994

Browse files
authored
V9.0.4/digest hotfix (#116)
🐛 removed quoted string values for the following parameters: stale and algorithm 🐛 removed quoted string for the following parameters: algorithm, qop and nc 🐛 removed statle from the parameters and marked prev. code obsolete 🐛 fixed bugs mentioned in #115 💬 updated community health pages 📦 updated NuGet package definition ✅ updated test accordingly
1 parent aa6c2f0 commit 57bc994

File tree

7 files changed

+69
-15
lines changed

7 files changed

+69
-15
lines changed

.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Availability: .NET 9 and .NET 8
44
# ALM
55
- CHANGED Dependencies to latest and greatest with respect to TFMs
66

7+
# Bug Fixes
8+
- FIXED DigestAuthenticationHandler class in the Cuemon.AspNetCore.Authentication.Digest namespace to remove quoted string values for the following parameters: stale and algorithm
9+
- FIXED DigestAuthenticationMiddleware class in the Cuemon.AspNetCore.Authentication.Digest namespace to remove quoted string values for the following parameters: stale and algorithm
10+
- FIXED DigestAuthorizationHeader class in the Cuemon.AspNetCore.Authentication.Digest namespace to remove quoted string values for the following parameters: algorithm, qop and nc
11+
- FIXED DigestAuthorizationHeader class in the Cuemon.AspNetCore.Authentication.Digest namespace so stale is removed from the parameters including marking previous code obsolete
12+
- FIXED DigestAuthorizationHeader class in the Cuemon.AspNetCore.Authentication.Digest namespace to accommodate the issues mentioned in https://github.com/gimlichael/Cuemon/issues/115
13+
 
714
Version 9.0.3
815
Availability: .NET 9 and .NET 8
916

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66

77
For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.
88

9-
## [9.0.4] - 2025-04-09
9+
## [9.0.4] - 2025-04-10
1010

11-
This is a service update that focuses on package dependencies.
11+
This is a service update that focuses on package dependencies and a few bug fixes.
12+
13+
> [!WARNING]
14+
> 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.
15+
16+
### Fixed
17+
18+
- Updated the `DigestAuthenticationHandler` class in the `Cuemon.AspNetCore.Authentication.Digest` namespace to remove quoted string values for the `stale` and `algorithm` parameters,
19+
- Updated the `DigestAuthenticationMiddleware` class in the `Cuemon.AspNetCore.Authentication.Digest` namespace to remove quoted string values for the `stale` and `algorithm` parameters,
20+
- Updated the `DigestAuthorizationHeader` class in the `Cuemon.AspNetCore.Authentication.Digest` namespace to:
21+
- Remove quoted string values for the `algorithm`, `qop`, and `nc` parameters,
22+
- Exclude the `stale` parameter and mark the previous implementation as obsolete,
23+
- Address issues outlined in [GitHub Issue #115](https://github.com/gimlichael/Cuemon/issues/115).
1224

1325
## [9.0.3] - 2025-03-31
1426

src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop
6666
var nonceGenerator = Options.NonceGenerator;
6767
var staleNonce = Context.Items[DigestFields.Stale] as string ?? "false";
6868
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
69-
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)}\""));
69+
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)}"));
7070
await base.HandleChallengeAsync(properties).ConfigureAwait(false);
7171
}
7272
}

src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ await Decorator.Enclose(context).InvokeUnauthorizedExceptionAsync(Options, princ
6161
var nonceSecret = Options.NonceSecret;
6262
var nonceGenerator = Options.NonceGenerator;
6363
var staleNonce = dc.Items[DigestFields.Stale] as string ?? "false";
64-
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)}\""));
64+
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)}"));
6565
}).ConfigureAwait(false);
6666
}
6767

src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeader.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class DigestAuthorizationHeader : AuthorizationHeader
2727
public static DigestAuthorizationHeader Create(string authorizationHeader)
2828
{
2929
Validator.ThrowIfNullOrWhitespace(authorizationHeader);
30-
return new DigestAuthorizationHeader().Parse(authorizationHeader, o => o.CredentialsDelimiter = " ") as DigestAuthorizationHeader;
30+
return new DigestAuthorizationHeader().Parse(authorizationHeader, o => o.CredentialsDelimiter = ", ") as DigestAuthorizationHeader;
3131
}
3232

3333
/// <summary>
@@ -39,6 +39,33 @@ private DigestAuthorizationHeader() : base(Scheme)
3939
{
4040
}
4141

42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="DigestAuthorizationHeader"/> class.
44+
/// </summary>
45+
/// <param name="realm">The realm/credential scope that defines the remote resource.</param>
46+
/// <param name="nonce">The unique server generated string.</param>
47+
/// <param name="opaque">The string of data specified by the server.</param>
48+
/// <param name="algorithm">The algorithm used to produce the digest and an unkeyed digest.</param>
49+
/// <param name="userName">The username of the specified <paramref name="realm"/>.</param>
50+
/// <param name="uri">The effective request URI.</param>
51+
/// <param name="nc">The hexadecimal count of the number of requests the client has sent with the <paramref name="nonce"/> value.</param>
52+
/// <param name="cNonce">The unique client generated string.</param>
53+
/// <param name="qop">The "quality of protection" the client has applied to the message.</param>
54+
/// <param name="response">The computed response which proves that the user knows a password.</param>
55+
public DigestAuthorizationHeader(string realm, string nonce, string opaque, string algorithm, string userName, string uri, string nc, string cNonce, string qop, string response) : base(Scheme)
56+
{
57+
Realm = realm;
58+
Nonce = nonce;
59+
Opaque = opaque;
60+
Algorithm = algorithm;
61+
UserName = userName;
62+
Uri = uri;
63+
NC = nc;
64+
CNonce = cNonce;
65+
Qop = qop;
66+
Response = response;
67+
}
68+
4269
/// <summary>
4370
/// Initializes a new instance of the <see cref="DigestAuthorizationHeader"/> class.
4471
/// </summary>
@@ -53,6 +80,7 @@ private DigestAuthorizationHeader() : base(Scheme)
5380
/// <param name="cNonce">The unique client generated string.</param>
5481
/// <param name="qop">The "quality of protection" the client has applied to the message.</param>
5582
/// <param name="response">The computed response which proves that the user knows a password.</param>
83+
[Obsolete("This constructor is obsolete and will be removed in a future version. Use the 'DigestAuthorizationHeader' constructor without the 'stale' parameter instead.")]
5684
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)
5785
{
5886
Realm = realm;
@@ -132,6 +160,7 @@ public DigestAuthorizationHeader(string realm, string nonce, string opaque, stri
132160
/// Gets the case-insensitive flag indicating if the previous request from the client was rejected because the <see cref="Nonce"/> value was stale.
133161
/// </summary>
134162
/// <value>The case-insensitive flag indicating if the previous request from the client was rejected because the <see cref="Nonce"/> value was stale.</value>
163+
[Obsolete("This property is obsolete and will be removed in a future version.")]
135164
public string Stale { get; }
136165

137166
/// <summary>
@@ -151,8 +180,7 @@ protected override AuthorizationHeader ParseCore(IReadOnlyDictionary<string, str
151180
valid |= credentials.TryGetValue(DigestFields.QualityOfProtection, out var qop);
152181
valid |= credentials.TryGetValue(DigestFields.ClientNonce, out var cnonce);
153182
valid |= credentials.TryGetValue(DigestFields.NonceCount, out var nc);
154-
valid |= credentials.TryGetValue(DigestFields.Stale, out var stale);
155-
return valid ? new DigestAuthorizationHeader(realm, nonce, opaque, stale, algorithm, userName, uri, nc, cnonce, qop, response) : null;
183+
return valid ? new DigestAuthorizationHeader(realm, nonce, opaque, algorithm, userName, uri, nc, cnonce, qop, response) : null;
156184
}
157185

158186
/// <summary>
@@ -166,19 +194,25 @@ public override string ToString()
166194
AppendField(sb, DigestFields.Realm, Realm);
167195
AppendField(sb, DigestFields.Nonce, Nonce);
168196
AppendField(sb, DigestFields.DigestUri, Uri);
169-
AppendField(sb, DigestFields.QualityOfProtection, Qop);
170-
AppendField(sb, DigestFields.NonceCount, NC);
197+
AppendField(sb, DigestFields.QualityOfProtection, Qop, false);
198+
AppendField(sb, DigestFields.NonceCount, NC, false);
171199
AppendField(sb, DigestFields.ClientNonce, CNonce);
172200
AppendField(sb, DigestFields.Response, Response);
173201
AppendField(sb, DigestFields.Opaque, Opaque);
174-
AppendField(sb, DigestFields.Stale, Stale);
175-
AppendField(sb, DigestFields.Algorithm, Algorithm);
176-
return sb.ToString();
202+
AppendField(sb, DigestFields.Algorithm, Algorithm, false);
203+
return sb.ToString().TrimEnd(',');
204+
}
205+
206+
private static void AppendField(StringBuilder sb, string fn, string fv, bool useQuotedStringSyntax = true)
207+
{
208+
if (!string.IsNullOrWhiteSpace(fv)) { sb.Append(CultureInfo.InvariantCulture, $" {fn}={Parse(fv, useQuotedStringSyntax)},"); }
177209
}
178210

179-
private static void AppendField(StringBuilder sb, string fn, string fv)
211+
private static string Parse(string value, bool useQuotedStringSyntax)
180212
{
181-
if (!string.IsNullOrWhiteSpace(fv)) { sb.Append(CultureInfo.InvariantCulture, $" {fn}=\"{fv}\""); }
213+
return useQuotedStringSyntax
214+
? $"\"{value}\""
215+
: value;
182216
}
183217
}
184218
}

src/Cuemon.AspNetCore.Authentication/GlobalSuppressions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
[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)")]
99
[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)")]
1010
[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Intentional as these embark on IDecorator.", Scope = "namespace", Target = "~N:Cuemon.AspNetCore.Authentication")]
11+
[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)")]

test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ public async void AuthorizationResponseHandler_DigestScheme_ShouldRenderResponse
651651
TestOutput.WriteLine(content);
652652

653653
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
654-
Assert.Equal("Digest realm=\"AuthenticationServer\", qop=\"auth, auth-int\", nonce=\"MjAyNC0wMi0wMyAyMTo1NjoyMVo6MDlhZTFhZDIyZGE4ZGExYTAxMmVkMzMwZWJlMzVkOTNlOGNmYTFmN2FiMzU5YzY0YTUwODFjZThkYjM1NzIwZA==\", opaque=\"dd1867244f862b1f858784a9b276d609\", stale=\"false\", algorithm=\"SHA-256\"", result.Headers.WwwAuthenticate.ToString());
654+
Assert.Equal("Digest realm=\"AuthenticationServer\", qop=\"auth, auth-int\", nonce=\"MjAyNC0wMi0wMyAyMTo1NjoyMVo6MDlhZTFhZDIyZGE4ZGExYTAxMmVkMzMwZWJlMzVkOTNlOGNmYTFmN2FiMzU5YzY0YTUwODFjZThkYjM1NzIwZA==\", opaque=\"dd1867244f862b1f858784a9b276d609\", stale=false, algorithm=SHA-256", result.Headers.WwwAuthenticate.ToString());
655655
if (sensitivityDetails == FaultSensitivityDetails.All)
656656
{
657657
Assert.Equal("""

0 commit comments

Comments
 (0)