Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
Expand All @@ -39,6 +39,33 @@ private DigestAuthorizationHeader() : base(Scheme)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DigestAuthorizationHeader"/> class.
/// </summary>
/// <param name="realm">The realm/credential scope that defines the remote resource.</param>
/// <param name="nonce">The unique server generated string.</param>
/// <param name="opaque">The string of data specified by the server.</param>
/// <param name="algorithm">The algorithm used to produce the digest and an unkeyed digest.</param>
/// <param name="userName">The username of the specified <paramref name="realm"/>.</param>
/// <param name="uri">The effective request URI.</param>
/// <param name="nc">The hexadecimal count of the number of requests the client has sent with the <paramref name="nonce"/> value.</param>
/// <param name="cNonce">The unique client generated string.</param>
/// <param name="qop">The "quality of protection" the client has applied to the message.</param>
/// <param name="response">The computed response which proves that the user knows a password.</param>
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;
}

/// <summary>
/// Initializes a new instance of the <see cref="DigestAuthorizationHeader"/> class.
/// </summary>
Expand All @@ -53,6 +80,7 @@ private DigestAuthorizationHeader() : base(Scheme)
/// <param name="cNonce">The unique client generated string.</param>
/// <param name="qop">The "quality of protection" the client has applied to the message.</param>
/// <param name="response">The computed response which proves that the user knows a password.</param>
[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;
Expand Down Expand Up @@ -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 <see cref="Nonce"/> value was stale.
/// </summary>
/// <value>The case-insensitive flag indicating if the previous request from the client was rejected because the <see cref="Nonce"/> value was stale.</value>
[Obsolete("This property is obsolete and will be removed in a future version.")]
public string Stale { get; }

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

/// <summary>
Expand All @@ -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;
}
}
}
1 change: 1 addition & 0 deletions src/Cuemon.AspNetCore.Authentication/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)")]
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand Down
Loading