diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs index 298d9b05..46a90bab 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs @@ -142,8 +142,7 @@ public class Startup .EnableLogoutEndpointPassthrough() .EnableTokenEndpointPassthrough() .EnableUserinfoEndpointPassthrough() - .EnableVerificationEndpointPassthrough() - .DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement. + .EnableVerificationEndpointPassthrough(); // Note: if you don't want to specify a client_id when sending // a token or revocation request, uncomment the following line: diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 28aeb044..78ec55cf 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1384,6 +1384,12 @@ Consider registering a certificate using 'services.AddOpenIddict().AddClient().A The specified grant type ({0}) is not listed as a supported grant type in the server configuration. If the error persists, ensure the supported grant types listed in the authorization server configuration are appropriate. + + Challenge operations cannot be triggered from non-HTTPS endpoints when the transport security requirement is enforced. While not recommended (as HTTPS is required for SameSite=None cookies to work correctly in most browsers), the transport security requirement can be disabled in the ASP.NET Core or OWIN options via 'services.AddOpenIddict().AddClient().UseAspNetCore().DisableTransportSecurityRequirement()' or 'services.AddOpenIddict().AddClient().UseOwin().DisableTransportSecurityRequirement()'. + + + Sign-out operations cannot be triggered from non-HTTPS endpoints when the transport security requirement is enforced. While not recommended (as HTTPS is required for SameSite=None cookies to work correctly in most browsers), the transport security requirement can be disabled in the ASP.NET Core or OWIN options via 'services.AddOpenIddict().AddClient().UseAspNetCore().DisableTransportSecurityRequirement()' or 'services.AddOpenIddict().AddClient().UseOwin().DisableTransportSecurityRequirement()'. + The security token is missing. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs index 27647d5a..83a9c1fd 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs @@ -47,6 +47,13 @@ public sealed class OpenIddictClientAspNetCoreBuilder return this; } + /// + /// Disables the transport security requirement (HTTPS). + /// + /// The instance. + public OpenIddictClientAspNetCoreBuilder DisableTransportSecurityRequirement() + => Configure(options => options.DisableTransportSecurityRequirement = true); + /// /// Enables the pass-through mode for the OpenID Connect post-logout redirection endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs index 33346e12..da07fee7 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs @@ -43,6 +43,7 @@ public static class OpenIddictClientAspNetCoreExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Register the option initializer used by the OpenIddict ASP.NET Core client integration services. // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs index cbc95cab..acaa53fa 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlerFilters.cs @@ -117,4 +117,25 @@ public static class OpenIddictClientAspNetCoreHandlerFilters return new(_options.CurrentValue.EnableStatusCodePagesIntegration); } } + + /// + /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. + /// + public sealed class RequireTransportSecurityRequirementEnabled : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireTransportSecurityRequirementEnabled(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!_options.CurrentValue.DisableTransportSecurityRequirement); + } + } } diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs index ef51615e..ec789efa 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs @@ -34,6 +34,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers * Top-level request processing: */ InferEndpointType.Descriptor, + ValidateTransportSecurityRequirement.Descriptor, /* * Authentication processing: @@ -45,12 +46,14 @@ public static partial class OpenIddictClientAspNetCoreHandlers * Challenge processing: */ ResolveHostChallengeProperties.Descriptor, + ValidateTransportSecurityRequirementForChallenge.Descriptor, GenerateLoginCorrelationCookie.Descriptor, /* * Sign-out processing: */ ResolveHostSignOutProperties.Descriptor, + ValidateTransportSecurityRequirementForSignOut.Descriptor, GenerateLogoutCorrelationCookie.Descriptor) .AddRange(Authentication.DefaultHandlers) .AddRange(Session.DefaultHandlers); @@ -144,6 +147,57 @@ public static partial class OpenIddictClientAspNetCoreHandlers } } + /// + /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public sealed class ValidateTransportSecurityRequirement : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + + // Don't require that the host be present if the request is not handled by OpenIddict. + if (context.EndpointType is OpenIddictClientEndpointType.Unknown) + { + return default; + } + + if (!request.IsHttps) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2083), + uri: SR.FormatID8000(SR.ID2083)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting OpenID Connect requests from GET or POST HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. @@ -551,6 +605,46 @@ public static partial class OpenIddictClientAspNetCoreHandlers } } + /// + /// Contains the logic responsible for preventing challenge operations from being triggered from non-HTTPS endpoints. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public sealed class ValidateTransportSecurityRequirementForChallenge : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateChallengeDemand.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + + if (!request.IsHttps) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0364)); + } + + return default; + } + } + /// /// Contains the logic responsible for creating a correlation cookie that serves as a /// protection against state token injection, forged requests and session fixation attacks. @@ -744,6 +838,46 @@ public static partial class OpenIddictClientAspNetCoreHandlers } } + /// + /// Contains the logic responsible for preventing sign-out operations from being triggered from non-HTTPS endpoints. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public sealed class ValidateTransportSecurityRequirementForSignOut : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateSignOutDemand.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessSignOutContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + + if (!request.IsHttps) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0365)); + } + + return default; + } + } + /// /// Contains the logic responsible for creating a correlation cookie that serves as a /// protection against state token injection, forged requests and denial of service attacks. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs index 1abd5033..a9e2a55c 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs @@ -13,6 +13,13 @@ namespace OpenIddict.Client.AspNetCore; /// public sealed class OpenIddictClientAspNetCoreOptions : AuthenticationSchemeOptions { + /// + /// Gets or sets a boolean indicating whether incoming requests arriving on insecure endpoints should be + /// rejected and whether challenge and sign-out operations can be triggered from non-HTTPS endpoints. + /// By default, this property is set to to help mitigate man-in-the-middle attacks. + /// + public bool DisableTransportSecurityRequirement { get; set; } + /// /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the post-logout redirection endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs index a865a059..f39234b3 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs @@ -47,6 +47,13 @@ public sealed class OpenIddictClientOwinBuilder return this; } + /// + /// Disables the transport security requirement (HTTPS). + /// + /// The instance. + public OpenIddictClientOwinBuilder DisableTransportSecurityRequirement() + => Configure(options => options.DisableTransportSecurityRequirement = true); + /// /// Enables the pass-through mode for the OpenID Connect post-logout redirection endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs index c4599d79..6335092d 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs @@ -45,6 +45,7 @@ public static class OpenIddictClientOwinExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Register the option initializer used by the OpenIddict OWIN client integration services. // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlerFilters.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlerFilters.cs index 8c461f45..df79287c 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlerFilters.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlerFilters.cs @@ -96,4 +96,25 @@ public static class OpenIddictClientOwinHandlerFilters return new(context.Transaction.GetOwinRequest() is not null); } } + + /// + /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. + /// + public sealed class RequireTransportSecurityRequirementEnabled : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireTransportSecurityRequirementEnabled(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!_options.CurrentValue.DisableTransportSecurityRequirement); + } + } } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs index ae8aab0e..9aa61780 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs @@ -30,6 +30,7 @@ public static partial class OpenIddictClientOwinHandlers * Top-level request processing: */ InferEndpointType.Descriptor, + ValidateTransportSecurityRequirement.Descriptor, /* * Authentication processing: @@ -41,12 +42,14 @@ public static partial class OpenIddictClientOwinHandlers * Challenge processing: */ ResolveHostChallengeProperties.Descriptor, + ValidateTransportSecurityRequirementForChallenge.Descriptor, GenerateLoginCorrelationCookie.Descriptor, /* * Sign-out processing: */ ResolveHostSignOutProperties.Descriptor, + ValidateTransportSecurityRequirementForSignOut.Descriptor, GenerateLogoutCorrelationCookie.Descriptor) .AddRange(Authentication.DefaultHandlers) .AddRange(Session.DefaultHandlers); @@ -143,6 +146,57 @@ public static partial class OpenIddictClientOwinHandlers } } + /// + /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public sealed class ValidateTransportSecurityRequirement : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Don't require that the host be present if the request is not handled by OpenIddict. + if (context.EndpointType is OpenIddictClientEndpointType.Unknown) + { + return default; + } + + if (!request.IsSecure) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2083), + uri: SR.FormatID8000(SR.ID2083)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting OpenID Connect requests from GET or POST HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. @@ -580,6 +634,46 @@ public static partial class OpenIddictClientOwinHandlers } } + /// + /// Contains the logic responsible for preventing challenge operations from being triggered from non-HTTPS endpoints. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public sealed class ValidateTransportSecurityRequirementForChallenge : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateChallengeDemand.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + if (!request.IsSecure) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0364)); + } + + return default; + } + } + /// /// Contains the logic responsible for creating a correlation cookie that serves as a /// protection against state token injection, forged requests and session fixation attacks. @@ -799,6 +893,46 @@ public static partial class OpenIddictClientOwinHandlers } } + /// + /// Contains the logic responsible for preventing sign-out operations from being triggered from non-HTTPS endpoints. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public sealed class ValidateTransportSecurityRequirementForSignOut : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateSignOutDemand.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessSignOutContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + if (!request.IsSecure) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0365)); + } + + return default; + } + } + /// /// Contains the logic responsible for creating a correlation cookie that serves as a /// protection against state token injection, forged requests and denial of service attacks. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs index e18380f5..a8ff759a 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs @@ -20,6 +20,13 @@ public sealed class OpenIddictClientOwinOptions : AuthenticationOptions : base(OpenIddictClientOwinDefaults.AuthenticationType) => AuthenticationMode = AuthenticationMode.Passive; + /// + /// Gets or sets a boolean indicating whether incoming requests arriving on insecure endpoints should be + /// rejected and whether challenge and sign-out operations can be triggered from non-HTTPS endpoints. + /// By default, this property is set to to help mitigate man-in-the-middle attacks. + /// + public bool DisableTransportSecurityRequirement { get; set; } + /// /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the post-logout redirection endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index d05707b0..73e19e4d 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -262,7 +262,6 @@ public static partial class OpenIddictServerAspNetCoreHandlers return default; } - // Reject authorization requests sent without transport security. if (!request.IsHttps) { context.Reject( diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 8389b4ac..bf1904e6 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -260,7 +260,6 @@ public static partial class OpenIddictServerOwinHandlers return default; } - // Reject authorization requests sent without transport security. if (!request.IsSecure) { context.Reject(