Browse Source

Rework the correlation cookie mechanism to use the nonce as the cookie name and store the request forgery protection in the cookie value

pull/1561/head
Kévin Chalet 3 years ago
parent
commit
efc4ff1c72
  1. 27
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 140
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  3. 138
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  4. 10
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  5. 295
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  6. 12
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  7. 17
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  8. 15
      src/OpenIddict.Server/OpenIddictServerHandlers.cs

27
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1346,6 +1346,15 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID0351" xml:space="preserve">
<value>The '{0}' instance returned by CryptoConfig.CreateFromName() is not suitable for the requested operation. When registering a custom implementation of a cryptographic algorithm, make sure it inherits from the correct base type and uses the correct name (e.g "OpenIddict RSA Cryptographic Provider" implementations must derive from System.Security.Cryptography.RSA).</value>
</data>
<data name="ID0352" xml:space="preserve">
<value>The nonce cannot be resolved from the challenge context.</value>
</data>
<data name="ID0353" xml:space="preserve">
<value>The nonce cannot be resolved from the sign-out context.</value>
</data>
<data name="ID0354" xml:space="preserve">
<value>The nonce cannot be resolved from the state token.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -1832,6 +1841,12 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID2162" xml:space="preserve">
<value>An unsupported response was returned by the remote authorization server.</value>
</data>
<data name="ID2163" xml:space="preserve">
<value>The correlation cookie is invalid or malformed.</value>
</data>
<data name="ID2164" xml:space="preserve">
<value>The request forgery protection is missing or doesn't contain the expected value.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -1880,6 +1895,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID4015" xml:space="preserve">
<value>The password shouldn't be null or empty at this point.</value>
</data>
<data name="ID4016" xml:space="preserve">
<value>The number of written bytes ({0}) doesn't match the expected value ({1}).</value>
</data>
<data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value>
</data>
@ -2467,6 +2485,15 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6208" xml:space="preserve">
<value>The authorization request was rejected by the remote authorization server: {Response}.</value>
</data>
<data name="ID6209" xml:space="preserve">
<value>The request forgery protection is missing or doesn't contain the expected value, which may indicate a session fixation attack.</value>
</data>
<data name="ID6210" xml:space="preserve">
<value>The nonce claim present in the frontchannel identity token doesn't contain the expected value, which may indicate a replay or token injection attack.</value>
</data>
<data name="ID6211" xml:space="preserve">
<value>The nonce claim present in the backchannel identity doesn't contain the expected value, which may indicate a replay or token injection attack.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

140
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
@ -15,6 +16,7 @@ using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties;
@ -36,7 +38,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
/*
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
ResolveRequestForgeryProtection.Descriptor,
ValidateEndpointUri.Descriptor,
/*
@ -223,15 +225,14 @@ public static partial class OpenIddictClientAspNetCoreHandlers
}
/// <summary>
/// Contains the logic responsible for validating the correlation cookie that serves as a protection
/// against state token injection, forged requests, denial of service and session fixation attacks.
/// Contains the logic responsible for resolving the request forgery protection from the correlation cookie.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ValidateCorrelationCookie : IOpenIddictClientHandler<ProcessAuthenticationContext>
public class ResolveRequestForgeryProtection : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientAspNetCoreOptions> _options;
public ValidateCorrelationCookie(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
public ResolveRequestForgeryProtection(IOptionsMonitor<OpenIddictClientAspNetCoreOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
@ -241,8 +242,8 @@ public static partial class OpenIddictClientAspNetCoreHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateCorrelationCookie>()
.SetOrder(ValidateStateToken.Descriptor.Order + 500)
.UseSingletonHandler<ResolveRequestForgeryProtection>()
.SetOrder(ValidateRequestForgeryProtection.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -261,36 +262,30 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Resolve the request forgery protection from the state token principal.
var identifier = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection);
if (string.IsNullOrEmpty(identifier))
// Resolve the nonce from the state token principal.
var nonce = context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce);
if (string.IsNullOrEmpty(nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0339));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0354));
}
// Resolve the cookie builder from the OWIN integration options.
var builder = _options.CurrentValue.CookieBuilder;
// Compute the name of the cookie name based on the prefix set in the options
// and the random request forgery protection claim restored from the state.
// Compute the name of the cookie name based on the prefix and the random nonce.
var name = new StringBuilder(builder.Name)
.Append(Separators.Dot)
.Append(identifier)
.Append(nonce)
.ToString();
// Try to find the cookie matching the request forgery protection stored in the state.
// The correlation cookie serves as a binding mechanism ensuring that a state token
// stolen from an authorization response with the other parameters cannot be validly
// used without sending the matching correlation identifier used as the cookie name.
//
// If the cookie cannot be found, this may indicate that the authorization response
// is unsolicited and potentially malicious or be caused by an invalid or unadequate
// same-site configuration.
// Try to find the correlation cookie matching the nonce stored in the state. If the cookie
// cannot be found, this may indicate that the authorization response is unsolicited and
// potentially malicious or be caused by an invalid or unadequate same-site configuration.
//
// In any case, the authentication demand MUST be rejected as it's impossible to ensure
// it's not an injection or session fixation attack without the correlation cookie.
var value = request.Cookies[name];
if (string.IsNullOrEmpty(value) || !string.Equals(value, "v1", StringComparison.Ordinal))
if (string.IsNullOrEmpty(value))
{
context.Reject(
error: Errors.InvalidRequest,
@ -300,6 +295,49 @@ public static partial class OpenIddictClientAspNetCoreHandlers
return default;
}
try
{
// Extract the payload and validate the version marker.
var payload = Base64UrlEncoder.DecodeBytes(value);
if (payload.Length < (1 + sizeof(uint)) || payload[0] is not 0x01)
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2163),
uri: SR.FormatID8000(SR.ID2163));
return default;
}
// Extract the length of the request forgery protection.
var length = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.AsSpan(1, sizeof(uint)));
if (length is 0 || length != (payload.Length - (1 + sizeof(uint))))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2163),
uri: SR.FormatID8000(SR.ID2163));
return default;
}
// Note: since the correlation cookie is not protected against tampering, an unexpected
// value may be present in the cookie payload and this call may return a string whose
// length doesn't match the expected value. In any case, any tampering attempt will be
// detected when comparing the resolved value with the expected value stored in the state.
context.RequestForgeryProtection = Encoding.UTF8.GetString(payload, index: 5, length);
}
catch
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2163),
uri: SR.FormatID8000(SR.ID2163));
return default;
}
// Return a response header asking the browser to delete the state cookie.
//
// Note: when deleting a cookie, the same options used when creating it MUST be specified.
@ -323,7 +361,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ValidateCorrelationCookie.Descriptor.Order + 500)
.SetOrder(ResolveRequestForgeryProtection.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -553,6 +591,11 @@ public static partial class OpenIddictClientAspNetCoreHandlers
// a different protection strategy can remove this handler from the handlers list and add
// a custom one using a different approach (e.g by storing the value in the session state).
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0352));
}
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0343));
@ -573,15 +616,29 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var options = builder.Build(response.HttpContext);
options.Expires ??= context.StateTokenPrincipal.GetExpirationDate();
// Compute a collision-resistant and hard-to-guess cookie name based on the prefix set
// in the options and the random request forgery protection claim generated earlier.
// Compute a collision-resistant and hard-to-guess cookie name using the nonce.
var name = new StringBuilder(builder.Name)
.Append(Separators.Dot)
.Append(context.RequestForgeryProtection)
.Append(context.Nonce)
.ToString();
// Create the cookie payload containing...
var count = Encoding.UTF8.GetByteCount(context.RequestForgeryProtection);
var payload = new byte[1 + sizeof(uint) + count];
// ... the version marker identifying the binary format used to create the payload (1 byte).
payload[0] = 0x01;
// ... the length of the request forgery protection (4 bytes).
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), (uint) count);
// ... the request forgery protection (variable length).
var written = Encoding.UTF8.GetBytes(s: context.RequestForgeryProtection, charIndex: 0,
charCount: context.RequestForgeryProtection.Length, bytes: payload, byteIndex: 5);
Debug.Assert(written == count, SR.FormatID4016(written, count));
// Add the correlation cookie to the response headers.
response.Cookies.Append(name, "v1", options);
response.Cookies.Append(name, Base64UrlEncoder.Encode(payload), options);
return default;
}
@ -726,6 +783,11 @@ public static partial class OpenIddictClientAspNetCoreHandlers
// a different protection strategy can remove this handler from the handlers list and add
// a custom one using a different approach (e.g by storing the value in the session state).
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0353));
}
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0344));
@ -746,15 +808,29 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var options = builder.Build(response.HttpContext);
options.Expires ??= context.StateTokenPrincipal.GetExpirationDate();
// Compute a collision-resistant and hard-to-guess cookie name based on the prefix set
// in the options and the random request forgery protection claim generated earlier.
// Compute a collision-resistant and hard-to-guess cookie name using the nonce.
var name = new StringBuilder(builder.Name)
.Append(Separators.Dot)
.Append(context.RequestForgeryProtection)
.Append(context.Nonce)
.ToString();
// Create the cookie payload containing...
var count = Encoding.UTF8.GetByteCount(context.RequestForgeryProtection);
var payload = new byte[1 + sizeof(uint) + count];
// ... the version marker identifying the binary format used to create the payload (1 byte).
payload[0] = 0x01;
// ... the length of the request forgery protection (4 bytes).
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), (uint) count);
// ... the request forgery protection (variable length).
var written = Encoding.UTF8.GetBytes(s: context.RequestForgeryProtection, charIndex: 0,
charCount: context.RequestForgeryProtection.Length, bytes: payload, byteIndex: 5);
Debug.Assert(written == count, SR.FormatID4016(written, count));
// Add the correlation cookie to the response headers.
response.Cookies.Append(name, "v1", options);
response.Cookies.Append(name, Base64UrlEncoder.Encode(payload), options);
return default;
}

138
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
@ -13,6 +14,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using Owin;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
@ -32,7 +34,7 @@ public static partial class OpenIddictClientOwinHandlers
/*
* Authentication processing:
*/
ValidateCorrelationCookie.Descriptor,
ResolveRequestForgeryProtection.Descriptor,
ValidateEndpointUri.Descriptor,
/*
@ -222,15 +224,14 @@ public static partial class OpenIddictClientOwinHandlers
}
/// <summary>
/// Contains the logic responsible for validating the correlation cookie that serves as a
/// protection against state token injection, forged requests and session fixation attacks.
/// Contains the logic responsible for resolving the request forgery protection from the correlation cookie.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ValidateCorrelationCookie : IOpenIddictClientHandler<ProcessAuthenticationContext>
public class ResolveRequestForgeryProtection : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictClientOwinOptions> _options;
public ValidateCorrelationCookie(IOptionsMonitor<OpenIddictClientOwinOptions> options)
public ResolveRequestForgeryProtection(IOptionsMonitor<OpenIddictClientOwinOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
@ -240,7 +241,7 @@ public static partial class OpenIddictClientOwinHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateCorrelationCookie>()
.UseSingletonHandler<ResolveRequestForgeryProtection>()
.SetOrder(ValidateStateToken.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -260,11 +261,11 @@ public static partial class OpenIddictClientOwinHandlers
var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Resolve the request forgery protection from the state token principal.
var identifier = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection);
if (string.IsNullOrEmpty(identifier))
// Resolve the nonce from the state token principal.
var nonce = context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce);
if (string.IsNullOrEmpty(nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0339));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0354));
}
// Resolve the cookie manager and the cookie options from the OWIN integration options.
@ -272,26 +273,20 @@ public static partial class OpenIddictClientOwinHandlers
_options.CurrentValue.CookieManager,
_options.CurrentValue.CookieOptions);
// Compute the name of the cookie name based on the prefix set in the options
// and the random request forgery protection claim restored from the state.
// Compute the name of the cookie name based on the prefix and the random nonce.
var name = new StringBuilder(_options.CurrentValue.CookieName)
.Append(Separators.Dot)
.Append(identifier)
.Append(nonce)
.ToString();
// Try to find the cookie matching the request forgery protection stored in the state.
// The correlation cookie serves as a binding mechanism ensuring that a state token
// stolen from an authorization response with the other parameters cannot be validly
// used without sending the matching correlation identifier used as the cookie name.
//
// If the cookie cannot be found, this may indicate that the authorization response
// is unsolicited and potentially malicious or be caused by an invalid or unadequate
// same-site configuration.
// Try to find the correlation cookie matching the nonce stored in the state. If the cookie
// cannot be found, this may indicate that the authorization response is unsolicited and
// potentially malicious or be caused by an invalid or unadequate same-site configuration.
//
// In any case, the authentication demand MUST be rejected as it's impossible to ensure
// it's not an injection or session fixation attack without the correlation cookie.
var value = manager.GetRequestCookie(request.Context, name);
if (string.IsNullOrEmpty(value) || !string.Equals(value, "v1", StringComparison.Ordinal))
if (string.IsNullOrEmpty(value))
{
context.Reject(
error: Errors.InvalidRequest,
@ -301,6 +296,49 @@ public static partial class OpenIddictClientOwinHandlers
return default;
}
try
{
// Extract the payload and validate the version marker.
var payload = Base64UrlEncoder.DecodeBytes(value);
if (payload.Length < (1 + sizeof(uint)) || payload[0] is not 0x01)
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2163),
uri: SR.FormatID8000(SR.ID2163));
return default;
}
// Extract the length of the request forgery protection.
var length = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.AsSpan(1, sizeof(uint)));
if (length is 0 || length != (payload.Length - (1 + sizeof(uint))))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2163),
uri: SR.FormatID8000(SR.ID2163));
return default;
}
// Note: since the correlation cookie is not protected against tampering, an unexpected
// value may be present in the cookie payload and this call may return a string whose
// length doesn't match the expected value. In any case, any tampering attempt will be
// detected when comparing the resolved value with the expected value stored in the state.
context.RequestForgeryProtection = Encoding.UTF8.GetString(payload, index: 5, length);
}
catch
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2163),
uri: SR.FormatID8000(SR.ID2163));
return default;
}
// Return a response header asking the browser to delete the state cookie.
//
// Note: when deleting a cookie, the same options used when creating it MUST be specified.
@ -331,7 +369,7 @@ public static partial class OpenIddictClientOwinHandlers
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ValidateCorrelationCookie.Descriptor.Order + 500)
.SetOrder(ResolveRequestForgeryProtection.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -582,6 +620,11 @@ public static partial class OpenIddictClientOwinHandlers
// a different protection strategy can remove this handler from the handlers list and add
// a custom one using a different approach (e.g by storing the value in the session state).
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0352));
}
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0343));
@ -594,20 +637,34 @@ public static partial class OpenIddictClientOwinHandlers
var response = context.Transaction.GetOwinRequest()?.Context.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Compute a collision-resistant and hard-to-guess cookie name based on the prefix set
// in the options and the random request forgery protection claim generated earlier.
// Compute a collision-resistant and hard-to-guess cookie name using the nonce.
var name = new StringBuilder(_options.CurrentValue.CookieName)
.Append(Separators.Dot)
.Append(context.RequestForgeryProtection)
.Append(context.Nonce)
.ToString();
// Create the cookie payload containing...
var count = Encoding.UTF8.GetByteCount(context.RequestForgeryProtection);
var payload = new byte[1 + sizeof(uint) + count];
// ... the version marker identifying the binary format used to create the payload (1 byte).
payload[0] = 0x01;
// ... the length of the request forgery protection (4 bytes).
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), (uint) count);
// ... the request forgery protection (variable length).
var written = Encoding.UTF8.GetBytes(s: context.RequestForgeryProtection, charIndex: 0,
charCount: context.RequestForgeryProtection.Length, bytes: payload, byteIndex: 5);
Debug.Assert(written == count, SR.FormatID4016(written, count));
// Resolve the cookie manager and the cookie options from the OWIN integration options.
var (manager, options) = (
_options.CurrentValue.CookieManager,
_options.CurrentValue.CookieOptions);
// Add the correlation cookie to the response headers.
manager.AppendResponseCookie(response.Context, name, "v1", new CookieOptions
manager.AppendResponseCookie(response.Context, name, Base64UrlEncoder.Encode(payload), new CookieOptions
{
Domain = options.Domain,
HttpOnly = options.HttpOnly,
@ -781,6 +838,11 @@ public static partial class OpenIddictClientOwinHandlers
// a different protection strategy can remove this handler from the handlers list and add
// a custom one using a different approach (e.g by storing the value in the session state).
if (string.IsNullOrEmpty(context.Nonce))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0353));
}
if (string.IsNullOrEmpty(context.RequestForgeryProtection))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0344));
@ -793,20 +855,34 @@ public static partial class OpenIddictClientOwinHandlers
var response = context.Transaction.GetOwinRequest()?.Context.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Compute a collision-resistant and hard-to-guess cookie name based on the prefix set
// in the options and the random request forgery protection claim generated earlier.
// Compute a collision-resistant and hard-to-guess cookie name using the nonce.
var name = new StringBuilder(_options.CurrentValue.CookieName)
.Append(Separators.Dot)
.Append(context.RequestForgeryProtection)
.Append(context.Nonce)
.ToString();
// Create the cookie payload containing...
var count = Encoding.UTF8.GetByteCount(context.RequestForgeryProtection);
var payload = new byte[1 + sizeof(uint) + count];
// ... the version marker identifying the binary format used to create the payload (1 byte).
payload[0] = 0x01;
// ... the length of the request forgery protection (4 bytes).
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), (uint) count);
// ... the request forgery protection (variable length).
var written = Encoding.UTF8.GetBytes(s: context.RequestForgeryProtection, charIndex: 0,
charCount: context.RequestForgeryProtection.Length, bytes: payload, byteIndex: 5);
Debug.Assert(written == count, SR.FormatID4016(written, count));
// Resolve the cookie manager and the cookie options from the OWIN integration options.
var (manager, options) = (
_options.CurrentValue.CookieManager,
_options.CurrentValue.CookieOptions);
// Add the correlation cookie to the response headers.
manager.AppendResponseCookie(response.Context, name, "v1", new CookieOptions
manager.AppendResponseCookie(response.Context, name, Base64UrlEncoder.Encode(payload), new CookieOptions
{
Domain = options.Domain,
HttpOnly = options.HttpOnly,

10
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -304,6 +304,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? ResponseType { get; set; }
/// <summary>
/// Gets or sets the request forgery protection resolved from the user session, if applicable.
/// </summary>
public string? RequestForgeryProtection { get; set; }
/// <summary>
/// Gets or sets the address of the token endpoint, if applicable.
/// </summary>
@ -890,6 +895,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? TargetLinkUri { get; set; }
/// <summary>
/// Gets or sets the nonce that will be used for the sign-out demand, if applicable.
/// </summary>
public string? Nonce { get; set; }
/// <summary>
/// Gets or sets the request forgery protection that will be stored in the state token, if applicable.
/// Note: this value MUST NOT be user-defined or extracted from any request and MUST be random

295
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -7,6 +7,7 @@
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
@ -35,6 +36,7 @@ public static partial class OpenIddictClientHandlers
ValidateStateToken.Descriptor,
RedeemStateTokenEntry.Descriptor,
ValidateStateTokenEndpointType.Descriptor,
ValidateRequestForgeryProtection.Descriptor,
ResolveClientRegistrationFromStateToken.Descriptor,
ValidateIssuerParameter.Descriptor,
HandleFrontchannelErrorResponse.Descriptor,
@ -119,6 +121,7 @@ public static partial class OpenIddictClientHandlers
AttachPostLogoutRedirectUri.Descriptor,
EvaluateGeneratedLogoutTokens.Descriptor,
AttachSignOutHostProperties.Descriptor,
AttachLogoutNonce.Descriptor,
AttachLogoutRequestForgeryProtection.Descriptor,
PrepareLogoutStateTokenPrincipal.Descriptor,
GenerateLogoutStateToken.Descriptor,
@ -511,6 +514,76 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the request forgery protection claim that serves as a
/// protection against state token injection, forged requests, denial of service and session fixation attacks.
/// </summary>
public class ValidateRequestForgeryProtection : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateRequestForgeryProtection>()
.SetOrder(ValidateStateTokenEndpointType.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the request forgery protection from the state token principal.
var comparand = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection);
if (string.IsNullOrEmpty(comparand))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0339));
}
// The request forgery protection serves as a binding mechanism ensuring that a
// state token stolen from an authorization response with the other parameters
// cannot be validly used without also sending the matching correlation identifier.
//
// If the request forgery protection couldn't be resolved at this point or doesn't
// match the expected value, this may indicate that the authentication demand is
// unsolicited and potentially malicious (or caused by an invalid or unadequate
// same-site configuration, if the authentication demand was handled by a web server).
//
// In any case, the authentication demand MUST be rejected as it's impossible to ensure
// it's not an injection or session fixation attack without the correct "rfp" value.
if (string.IsNullOrEmpty(context.RequestForgeryProtection) ||
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
!CryptographicOperations.FixedTimeEquals(
left: MemoryMarshal.AsBytes(comparand.AsSpan()),
right: MemoryMarshal.AsBytes(context.RequestForgeryProtection.AsSpan())))
#else
!Arrays.ConstantTimeAreEqual(
a: MemoryMarshal.AsBytes(comparand.AsSpan()).ToArray(),
b: MemoryMarshal.AsBytes(context.RequestForgeryProtection.AsSpan()).ToArray()))
#endif
{
context.Logger.LogWarning(SR.GetResourceString(SR.ID6209));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2164),
uri: SR.FormatID8000(SR.ID2164));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for resolving the client registration
/// based on the authorization server identity stored in the state token.
@ -524,7 +597,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ResolveClientRegistrationFromStateToken>()
.SetOrder(ValidateStateTokenEndpointType.Descriptor.Order + 1_000)
.SetOrder(ValidateRequestForgeryProtection.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
@ -1326,6 +1399,38 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Note: the OpenID Connect specification relies on nonces as a way to detect and
// prevent replay attacks by binding the returned identity token(s) to a specific
// random value sent by the client application as part of the authorization request.
//
// When Proof Key for Code Exchange is not supported or not available, nonces can
// also be used to detect authorization code or identity token injection attacks.
//
// For more information, see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
// and https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.5.3.2.
//
// While OpenIddict fully implements nonce support, its implementation slightly
// differs from the implementation suggested by the OpenID Connect specification:
//
// - Nonces are used internally as unique, per-authorization flow identifiers and
// are always considered required when using an interactive flow, independently
// of whether the authorization flow is an OAuth 2.0-only or OpenID Connect flow.
//
// - Instead of being stored as separate cookies as suggested by the specification,
// nonces are used by the ASP.NET Core and OWIN hosts to build a unique value
// for the name of the correlation cookie used with state tokens to prevent CSRF,
// which reduces the number of cookies used by the OpenIddict client web hosts.
//
// - Nonces are attached to the authorization requests AND stored in the state
// tokens so that the nonces and the state tokens form a 1 <-> 1 relationship,
// which forces sending the matching state to be able to validate identity tokens.
//
// - Replay detection is implemented by invalidating state tokens the very first time
// they are presented at the redirection endpoint, even if the response indicates
// an errored authorization response (e.g if the authorization demand was denied).
// Since nonce validation depends on the value stored in the state token, marking
// state tokens as already redeemed is enough to prevent nonces from being replayed.
Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
@ -1333,12 +1438,17 @@ public static partial class OpenIddictClientHandlers
FrontchannelIdentityTokenNonce: context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce),
StateTokenNonce: context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
{
// If no nonce if no present in the state token (e.g because the authorization server doesn't
// support OpenID Connect and response_type=code was negotiated), bypass the validation logic.
// If no nonce is present in the state token, bypass the validation logic.
case { StateTokenNonce: null or { Length: not > 0 } }:
return default;
// If a nonce was found in the state token but is not present in the identity token, return an error.
// If the request was not an OpenID Connect request but an identity token
// was returned nethertheless, don't require a nonce to be present.
case { FrontchannelIdentityTokenNonce: null or { Length: not > 0 } }
when !context.StateTokenPrincipal.HasScope(Scopes.OpenId):
return default;
// If the nonce is not present in the identity token, return an error.
case { FrontchannelIdentityTokenNonce: null or { Length: not > 0 } }:
context.Reject(
error: Errors.InvalidRequest,
@ -1350,10 +1460,18 @@ public static partial class OpenIddictClientHandlers
// If the two nonces don't match, return an error.
case { FrontchannelIdentityTokenNonce: string left, StateTokenNonce: string right } when
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)):
!CryptographicOperations.FixedTimeEquals(
left: MemoryMarshal.AsBytes(left.AsSpan()), // The nonce in the identity token is already hashed.
right: MemoryMarshal.AsBytes(Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.UTF8.GetBytes(right))).AsSpan())):
#else
!Arrays.ConstantTimeAreEqual(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)):
!Arrays.ConstantTimeAreEqual(
a: MemoryMarshal.AsBytes(left.AsSpan()).ToArray(), // The nonce in the identity token is already hashed.
b: MemoryMarshal.AsBytes(Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.UTF8.GetBytes(right))).AsSpan()).ToArray()):
#endif
context.Logger.LogWarning(SR.GetResourceString(SR.ID6210));
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2124(Claims.Nonce),
@ -1455,7 +1573,7 @@ public static partial class OpenIddictClientHandlers
}
}
static byte[] ComputeTokenHash(string algorithm, byte[] data)
static ReadOnlySpan<char> ComputeTokenHash(string algorithm, string token)
{
// Resolve the hash algorithm associated with the signing algorithm and compute the token
// hash. If an instance of the BCL hash algorithm cannot be resolved, throw an exception.
@ -1463,33 +1581,33 @@ public static partial class OpenIddictClientHandlers
{
SecurityAlgorithms.EcdsaSha256 or SecurityAlgorithms.HmacSha256 or
SecurityAlgorithms.RsaSha256 or SecurityAlgorithms.RsaSsaPssSha256
=> OpenIddictHelpers.ComputeSha256Hash(data),
=> OpenIddictHelpers.ComputeSha256Hash(Encoding.ASCII.GetBytes(token)),
SecurityAlgorithms.EcdsaSha384 or SecurityAlgorithms.HmacSha384 or
SecurityAlgorithms.RsaSha384 or SecurityAlgorithms.RsaSsaPssSha384
=> OpenIddictHelpers.ComputeSha384Hash(data),
=> OpenIddictHelpers.ComputeSha384Hash(Encoding.ASCII.GetBytes(token)),
SecurityAlgorithms.EcdsaSha512 or SecurityAlgorithms.HmacSha384 or
SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512
=> OpenIddictHelpers.ComputeSha512Hash(data),
=> OpenIddictHelpers.ComputeSha512Hash(Encoding.ASCII.GetBytes(token)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0293))
};
// Warning: only the left-most half of the access token and authorization code digest is used.
// See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken for more information.
return Encoding.ASCII.GetBytes(Base64UrlEncoder.Encode(hash, 0, hash.Length / 2));
return Base64UrlEncoder.Encode(hash, 0, hash.Length / 2).AsSpan();
}
static bool ValidateTokenHash(string algorithm, string token, string hash) =>
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
CryptographicOperations.FixedTimeEquals(
left: Encoding.ASCII.GetBytes(hash),
right: ComputeTokenHash(algorithm, Encoding.ASCII.GetBytes(token)));
left: MemoryMarshal.AsBytes(hash.AsSpan()),
right: MemoryMarshal.AsBytes(ComputeTokenHash(algorithm, token)));
#else
Arrays.ConstantTimeAreEqual(
a: Encoding.ASCII.GetBytes(hash),
b: ComputeTokenHash(algorithm, Encoding.ASCII.GetBytes(token)));
a: MemoryMarshal.AsBytes(hash.AsSpan()).ToArray(),
b: MemoryMarshal.AsBytes(ComputeTokenHash(algorithm, token)).ToArray());
#endif
return default;
}
@ -2584,6 +2702,38 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Note: the OpenID Connect specification relies on nonces as a way to detect and
// prevent replay attacks by binding the returned identity token(s) to a specific
// random value sent by the client application as part of the authorization request.
//
// When Proof Key for Code Exchange is not supported or not available, nonces can
// also be used to detect authorization code or identity token injection attacks.
//
// For more information, see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
// and https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.5.3.2.
//
// While OpenIddict fully implements nonce support, its implementation slightly
// differs from the implementation suggested by the OpenID Connect specification:
//
// - Nonces are used internally as unique, per-authorization flow identifiers and
// are always considered required when using an interactive flow, independently
// of whether the authorization flow is an OAuth 2.0-only or OpenID Connect flow.
//
// - Instead of being stored as separate cookies as suggested by the specification,
// nonces are used by the ASP.NET Core and OWIN hosts to build a unique value
// for the name of the correlation cookie used with state tokens to prevent CSRF,
// which reduces the number of cookies used by the OpenIddict client web hosts.
//
// - Nonces are attached to the authorization requests AND stored in the state
// tokens so that the nonces and the state tokens form a 1 <-> 1 relationship,
// which forces sending the matching state to be able to validate identity tokens.
//
// - Replay detection is implemented by invalidating state tokens the very first time
// they are presented at the redirection endpoint, even if the response indicates
// an errored authorization response (e.g if the authorization demand was denied).
// Since nonce validation depends on the value stored in the state token, marking
// state tokens as already redeemed is enough to prevent nonces from being replayed.
Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
@ -2591,12 +2741,17 @@ public static partial class OpenIddictClientHandlers
BackchannelIdentityTokenNonce: context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce),
StateTokenNonce: context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
{
// If no nonce if no present in the state token (e.g because the authorization server doesn't
// support OpenID Connect and response_type=code was negotiated), bypass the validation logic.
// If no nonce is present in the state token, bypass the validation logic.
case { StateTokenNonce: null or { Length: not > 0 } }:
return default;
// If a nonce was found in the state token but is not present in the identity token, return an error.
// If the request was not an OpenID Connect request but an identity token
// was returned nethertheless, don't require a nonce to be present.
case { BackchannelIdentityTokenNonce: null or { Length: not > 0 } }
when !context.StateTokenPrincipal.HasScope(Scopes.OpenId):
return default;
// If the nonce is not present in the identity token, return an error.
case { BackchannelIdentityTokenNonce: null or { Length: not > 0 } }:
context.Reject(
error: Errors.InvalidRequest,
@ -2608,10 +2763,18 @@ public static partial class OpenIddictClientHandlers
// If the two nonces don't match, return an error.
case { BackchannelIdentityTokenNonce: string left, StateTokenNonce: string right } when
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)):
!CryptographicOperations.FixedTimeEquals(
left: MemoryMarshal.AsBytes(left.AsSpan()), // The nonce in the identity token is already hashed.
right: MemoryMarshal.AsBytes(Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.UTF8.GetBytes(right))).AsSpan())):
#else
!Arrays.ConstantTimeAreEqual(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)):
!Arrays.ConstantTimeAreEqual(
a: MemoryMarshal.AsBytes(left.AsSpan()).ToArray(), // The nonce in the identity token is already hashed.
b: MemoryMarshal.AsBytes(Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.UTF8.GetBytes(right))).AsSpan()).ToArray()):
#endif
context.Logger.LogWarning(SR.GetResourceString(SR.ID6211));
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2128(Claims.Nonce),
@ -2677,7 +2840,7 @@ public static partial class OpenIddictClientHandlers
// Note: unlike frontchannel identity tokens, backchannel identity tokens are not expected to include
// an authorization code hash as no authorization code is normally returned from the token endpoint.
static byte[] ComputeTokenHash(string algorithm, byte[] data)
static ReadOnlySpan<char> ComputeTokenHash(string algorithm, string token)
{
// Resolve the hash algorithm associated with the signing algorithm and compute the token
// hash. If an instance of the BCL hash algorithm cannot be resolved, throw an exception.
@ -2685,33 +2848,33 @@ public static partial class OpenIddictClientHandlers
{
SecurityAlgorithms.EcdsaSha256 or SecurityAlgorithms.HmacSha256 or
SecurityAlgorithms.RsaSha256 or SecurityAlgorithms.RsaSsaPssSha256
=> OpenIddictHelpers.ComputeSha256Hash(data),
=> OpenIddictHelpers.ComputeSha256Hash(Encoding.ASCII.GetBytes(token)),
SecurityAlgorithms.EcdsaSha384 or SecurityAlgorithms.HmacSha384 or
SecurityAlgorithms.RsaSha384 or SecurityAlgorithms.RsaSsaPssSha384
=> OpenIddictHelpers.ComputeSha384Hash(data),
=> OpenIddictHelpers.ComputeSha384Hash(Encoding.ASCII.GetBytes(token)),
SecurityAlgorithms.EcdsaSha512 or SecurityAlgorithms.HmacSha384 or
SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512
=> OpenIddictHelpers.ComputeSha512Hash(data),
=> OpenIddictHelpers.ComputeSha512Hash(Encoding.ASCII.GetBytes(token)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0293))
};
// Warning: only the left-most half of the access token and authorization code digest is used.
// See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken for more information.
return Encoding.ASCII.GetBytes(Base64UrlEncoder.Encode(hash, 0, hash.Length / 2));
return Base64UrlEncoder.Encode(hash, 0, hash.Length / 2).AsSpan();
}
static bool ValidateTokenHash(string algorithm, string token, string hash) =>
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
CryptographicOperations.FixedTimeEquals(
left: Encoding.ASCII.GetBytes(hash),
right: ComputeTokenHash(algorithm, Encoding.ASCII.GetBytes(token)));
left: MemoryMarshal.AsBytes(hash.AsSpan()),
right: MemoryMarshal.AsBytes(ComputeTokenHash(algorithm, token)));
#else
Arrays.ConstantTimeAreEqual(
a: Encoding.ASCII.GetBytes(hash),
b: ComputeTokenHash(algorithm, Encoding.ASCII.GetBytes(token)));
a: MemoryMarshal.AsBytes(hash.AsSpan()).ToArray(),
b: MemoryMarshal.AsBytes(ComputeTokenHash(algorithm, token)).ToArray());
#endif
return default;
}
@ -4038,13 +4201,17 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// If the identity provider doesn't support OpenID Connect, don't attach a nonce.
if (!context.Configuration.ScopesSupported.Contains(Scopes.OpenId))
{
return default;
}
// Generate a new crypto-secure random identifier that will be used as the nonce.
//
// Note: a nonce is always generated for interactive grants, independently of whether
// the request is an OpenID Connect request or not, as it's used to identify each
// authorization demand and is needed by the web hosts like ASP.NET Core and OWIN
// to resolve the name of the correlation cookie used to prevent forged requests.
//
// If the request is an OpenID Connect request, the nonce will also be hashed and
// attached to the authorization request so that the identity provider can bind
// the issued identity tokens to the generated value, which helps detect token
// replay (and authorization code injection attacks when PKCE is not available).
context.Nonce = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
return default;
@ -4133,6 +4300,9 @@ public static partial class OpenIddictClientHandlers
else if (context.CodeChallengeMethod is CodeChallengeMethods.Sha256)
{
// Compute the SHA-256 hash of the code verifier and use it as the code challenge.
//
// Note: ASCII is deliberately used here, as it's the encoding required by the specification.
// For more information, see https://datatracker.ietf.org/doc/html/rfc7636#section-4.2.
context.CodeChallenge = Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(
Encoding.ASCII.GetBytes(context.CodeVerifier)));
}
@ -4243,6 +4413,9 @@ public static partial class OpenIddictClientHandlers
// Store the nonce in the state token so it can be later used to check whether
// the nonce extracted from the identity token matches the generated value.
//
// Note: the nonce is also used by the ASP.NET Core and OWIN hosts as a way
// to uniquely identify the name of the correlation cookie used for antiforgery.
principal.SetClaim(Claims.Private.Nonce, context.Nonce);
// Store the requested scopes in the state token.
@ -4395,7 +4568,16 @@ public static partial class OpenIddictClientHandlers
context.Request.Scope = string.Join(" ", context.Scopes);
}
context.Request.Nonce = context.Nonce;
// If the request is an OpenID Connect request, attach the nonce as a parameter.
//
// Note: the nonce is always hashed before being sent, as recommended the specification.
// See https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes for more information.
if (context.Scopes.Contains(Scopes.OpenId) && !string.IsNullOrEmpty(context.Nonce))
{
context.Request.Nonce = Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.UTF8.GetBytes(context.Nonce)));
}
context.Request.CodeChallenge = context.CodeChallenge;
context.Request.CodeChallengeMethod = context.CodeChallengeMethod;
@ -4670,7 +4852,7 @@ public static partial class OpenIddictClientHandlers
}
/// <summary>
/// Contains the logic responsible for attaching a request forgery protection to the authorization request.
/// Contains the logic responsible for attaching a request forgery protection to the logout request.
/// </summary>
public class AttachLogoutRequestForgeryProtection : IOpenIddictClientHandler<ProcessSignOutContext>
{
@ -4700,6 +4882,35 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching a nonce to the logout request.
/// </summary>
public class AttachLogoutNonce : IOpenIddictClientHandler<ProcessSignOutContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.UseSingletonHandler<AttachLogoutNonce>()
.SetOrder(AttachLogoutRequestForgeryProtection.Descriptor.Order + 1_000)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessSignOutContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Generate a new crypto-secure random identifier that will be used as the nonce.
context.Nonce = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
return default;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the logout state token, if one is going to be returned.
@ -4713,7 +4924,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessSignOutContext>()
.AddFilter<RequireLogoutStateTokenGenerated>()
.UseSingletonHandler<PrepareLogoutStateTokenPrincipal>()
.SetOrder(AttachLogoutRequestForgeryProtection.Descriptor.Order + 1_000)
.SetOrder(AttachLogoutNonce.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -4769,7 +4980,7 @@ public static partial class OpenIddictClientHandlers
principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri);
// Store the request forgery protection in the state token so it can be later used to
// ensure the authorization response sent to the redirection endpoint is not forged.
// ensure the logout response sent to the post-logout redirection endpoint is not forged.
principal.SetClaim(Claims.RequestForgeryProtection, context.RequestForgeryProtection);
// Store the optional return URL in the state token.
@ -4783,6 +4994,12 @@ public static partial class OpenIddictClientHandlers
// Store the post_logout_redirect_uri to allow comparing to the actual redirection URL.
principal.SetClaim(Claims.Private.PostLogoutRedirectUri, context.PostLogoutRedirectUri);
// Store the nonce in the state token.
//
// Note: the nonce is also used by the ASP.NET Core and OWIN hosts as a way
// to uniquely identify the name of the correlation cookie used for antiforgery.
principal.SetClaim(Claims.Private.Nonce, context.Nonce);
context.StateTokenPrincipal = principal;
return default;

12
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -1402,7 +1402,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
payload[0] = 0x01;
// Write the hashing algorithm version.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), algorithm switch
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, sizeof(uint)), algorithm switch
{
var name when name == HashAlgorithmName.SHA1 => 0,
var name when name == HashAlgorithmName.SHA256 => 1,
@ -1412,10 +1412,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
});
// Write the iteration count of the algorithm.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(5, 8), (uint) iterations);
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(5, sizeof(uint)), (uint) iterations);
// Write the size of the salt.
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(9, 12), (uint) salt.Length);
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(9, sizeof(uint)), (uint) salt.Length);
// Write the salt.
salt.CopyTo(payload.AsSpan(13));
@ -1482,7 +1482,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
}
// Read the hashing algorithm version.
var algorithm = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(1, 4)) switch
var algorithm = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(1, sizeof(uint))) switch
{
0 => HashAlgorithmName.SHA1,
1 => HashAlgorithmName.SHA256,
@ -1492,10 +1492,10 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
};
// Read the iteration count of the algorithm.
var iterations = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(5, 8));
var iterations = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(5, sizeof(uint)));
// Read the size of the salt and ensure it's more than 128 bits.
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, 12));
var saltLength = (int) BinaryPrimitives.ReadUInt32BigEndian(payload.Slice(9, sizeof(uint)));
if (saltLength < 128 / 8)
{
return false;

17
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -6,6 +6,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
@ -1548,11 +1549,11 @@ public static partial class OpenIddictServerHandlers
var comparand = context.Principal.GetClaim(Claims.Private.CodeChallengeMethod) switch
{
// Note: when using the "plain" code challenge method, no hashing is actually performed.
// In this case, the raw ASCII bytes of the verifier are directly compared to the challenge.
CodeChallengeMethods.Plain => Encoding.ASCII.GetBytes(context.Request.CodeVerifier),
// In this case, the raw bytes of the verifier are directly compared to the challenge.
CodeChallengeMethods.Plain => context.Request.CodeVerifier,
CodeChallengeMethods.Sha256 => Encoding.ASCII.GetBytes(Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.ASCII.GetBytes(context.Request.CodeVerifier)))),
CodeChallengeMethods.Sha256 => Base64UrlEncoder.Encode(
OpenIddictHelpers.ComputeSha256Hash(Encoding.ASCII.GetBytes(context.Request.CodeVerifier))),
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0268)),
@ -1562,9 +1563,13 @@ public static partial class OpenIddictServerHandlers
// Compare the verifier and the code challenge: if the two don't match, return an error.
// Note: to prevent timing attacks, a time-constant comparer is always used.
#if SUPPORTS_TIME_CONSTANT_COMPARISONS
if (!CryptographicOperations.FixedTimeEquals(comparand, Encoding.ASCII.GetBytes(challenge)))
if (!CryptographicOperations.FixedTimeEquals(
left: MemoryMarshal.AsBytes(comparand.AsSpan()),
right: MemoryMarshal.AsBytes(challenge.AsSpan())))
#else
if (!Arrays.ConstantTimeAreEqual(comparand, Encoding.ASCII.GetBytes(challenge)))
if (!Arrays.ConstantTimeAreEqual(
a: MemoryMarshal.AsBytes(comparand.AsSpan()).ToArray(),
b: MemoryMarshal.AsBytes(challenge.AsSpan()).ToArray()))
#endif
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6092), Parameters.CodeVerifier);

15
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -2710,7 +2710,7 @@ public static partial class OpenIddictServerHandlers
if (!string.IsNullOrEmpty(context.AccessToken))
{
var digest = ComputeHash(credentials, Encoding.ASCII.GetBytes(context.AccessToken));
var digest = ComputeTokenHash(credentials, context.AccessToken);
// Note: only the left-most half of the hash is used.
// See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
@ -2719,7 +2719,7 @@ public static partial class OpenIddictServerHandlers
if (!string.IsNullOrEmpty(context.AuthorizationCode))
{
var digest = ComputeHash(credentials, Encoding.ASCII.GetBytes(context.AuthorizationCode));
var digest = ComputeTokenHash(credentials, context.AuthorizationCode);
// Note: only the left-most half of the hash is used.
// See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
@ -2728,28 +2728,31 @@ public static partial class OpenIddictServerHandlers
return default;
static byte[] ComputeHash(SigningCredentials credentials, byte[] data) => credentials switch
static byte[] ComputeTokenHash(SigningCredentials credentials, string token) => credentials switch
{
// Note: ASCII is deliberately used here, as it's the encoding required by the specification.
// For more information, see https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken.
{ Digest: SecurityAlgorithms.Sha256 or SecurityAlgorithms.Sha256Digest } or
{ Algorithm: SecurityAlgorithms.EcdsaSha256 or SecurityAlgorithms.EcdsaSha256Signature } or
{ Algorithm: SecurityAlgorithms.HmacSha256 or SecurityAlgorithms.HmacSha256Signature } or
{ Algorithm: SecurityAlgorithms.RsaSha256 or SecurityAlgorithms.RsaSha256Signature } or
{ Algorithm: SecurityAlgorithms.RsaSsaPssSha256 or SecurityAlgorithms.RsaSsaPssSha256Signature }
=> OpenIddictHelpers.ComputeSha256Hash(data),
=> OpenIddictHelpers.ComputeSha256Hash(Encoding.ASCII.GetBytes(token)),
{ Digest: SecurityAlgorithms.Sha384 or SecurityAlgorithms.Sha384Digest } or
{ Algorithm: SecurityAlgorithms.EcdsaSha384 or SecurityAlgorithms.EcdsaSha384Signature } or
{ Algorithm: SecurityAlgorithms.HmacSha384 or SecurityAlgorithms.HmacSha384Signature } or
{ Algorithm: SecurityAlgorithms.RsaSha384 or SecurityAlgorithms.RsaSha384Signature } or
{ Algorithm: SecurityAlgorithms.RsaSsaPssSha384 or SecurityAlgorithms.RsaSsaPssSha384Signature }
=> OpenIddictHelpers.ComputeSha384Hash(data),
=> OpenIddictHelpers.ComputeSha384Hash(Encoding.ASCII.GetBytes(token)),
{ Digest: SecurityAlgorithms.Sha512 or SecurityAlgorithms.Sha512Digest } or
{ Algorithm: SecurityAlgorithms.EcdsaSha512 or SecurityAlgorithms.EcdsaSha512Signature } or
{ Algorithm: SecurityAlgorithms.HmacSha512 or SecurityAlgorithms.HmacSha512Signature } or
{ Algorithm: SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSha512Signature } or
{ Algorithm: SecurityAlgorithms.RsaSsaPssSha512 or SecurityAlgorithms.RsaSsaPssSha512Signature }
=> OpenIddictHelpers.ComputeSha512Hash(data),
=> OpenIddictHelpers.ComputeSha512Hash(Encoding.ASCII.GetBytes(token)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0267))
};

Loading…
Cancel
Save