From 69e9a7e7c3762aa08560bcae04b518445f3cf708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 20 Jun 2022 18:36:12 +0200 Subject: [PATCH] Update the OWIN hosts to automatically set SuppressFormsAuthenticationRedirect to true on challenges --- ...IddictClientOwinHandlers.Authentication.cs | 1 + .../OpenIddictClientOwinHandlers.cs | 69 +++++++++++++++++- ...IddictServerOwinHandlers.Authentication.cs | 1 + .../OpenIddictServerOwinHandlers.Device.cs | 2 + .../OpenIddictServerOwinHandlers.Discovery.cs | 2 + .../OpenIddictServerOwinHandlers.Exchange.cs | 1 + ...nIddictServerOwinHandlers.Introspection.cs | 1 + ...OpenIddictServerOwinHandlers.Revocation.cs | 1 + .../OpenIddictServerOwinHandlers.Session.cs | 1 + .../OpenIddictServerOwinHandlers.Userinfo.cs | 1 + .../OpenIddictServerOwinHandlers.cs | 69 +++++++++++++++++- .../OpenIddictValidationOwinHandlers.cs | 73 ++++++++++++++++++- 12 files changed, 217 insertions(+), 5 deletions(-) diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs index 89454951..8041557a 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.Authentication.cs @@ -34,6 +34,7 @@ public static partial class OpenIddictClientOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, ProcessPassthroughErrorResponse.Descriptor, ProcessLocalErrorResponse.Descriptor); diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs index d5f4a0a3..8c284fc3 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs @@ -716,6 +716,73 @@ public static partial class OpenIddictClientOwinHandlers } } + /// + /// Contains the logic responsible for suppressing the redirection applied by FormsAuthenticationModule, if necessary. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class SuppressFormsAuthenticationRedirect : IOpenIddictClientHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachOwinResponseChallenge.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext 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 response = context.Transaction.GetOwinRequest()?.Context.Response ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Similarly to the automatic authentication mode used by OWIN authentication middleware, + // the ASP.NET FormsAuthentication module aggressively intercepts 401 responses even if + // the request has already been fully handled by another component (like OpenIddict). + // To prevent that, this handler is responsible for suppressing the redirection enforced + // by FormsAuthenticationModule when the status code was set to 401 (the only status code + // used by the FormsAuthenticationModule) and the OWIN application is hosted on SystemWeb. + if (response.StatusCode is 401) + { + TrySuppressFormsAuthenticationRedirect(response.Environment); + } + + return default; + + static void TrySuppressFormsAuthenticationRedirect(IDictionary environment) + { + // Note: the OWIN host cannot depend on the OWIN SystemWeb package but a direct access + // to the underlying ASP.NET 4.x context is required to be able to disable the redirection + // enforced by the FormsAuthentication module. To work around that, the HttpContextBase + // instance is resolved from the OWIN environment and SuppressFormsAuthenticationRedirect + // is set to true using a dynamic runtime resolution (that uses reflection under the hood). + if (environment.TryGetValue("System.Web.HttpContextBase", out dynamic context)) + { + try + { + // Note: the SuppressFormsAuthenticationRedirect property was introduced in ASP.NET 4.5 + // and thus should always be present, as OpenIddict requires targeting ASP.NET >= 4.6.1. + context.Response.SuppressFormsAuthenticationRedirect = true; + } + + catch + { + } + } + } + } + } + /// /// Contains the logic responsible for attaching the appropriate HTTP response cache headers. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. @@ -729,7 +796,7 @@ public static partial class OpenIddictClientOwinHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(AttachOwinResponseChallenge.Descriptor.Order + 1_000) + .SetOrder(SuppressFormsAuthenticationRedirect.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs index b5184d45..fc37c3e8 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs @@ -45,6 +45,7 @@ public static partial class OpenIddictServerOwinHandlers RemoveCachedRequest.Descriptor, AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, ProcessFormPostResponse.Descriptor, ProcessQueryResponse.Descriptor, diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs index 49fe072e..d27aa142 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs @@ -26,6 +26,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor, @@ -45,6 +46,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, ProcessHostRedirectionResponse.Descriptor, ProcessPassthroughErrorResponse.Descriptor, diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs index b7e76f9c..ac32e5dd 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Discovery.cs @@ -23,6 +23,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor, @@ -36,6 +37,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor); } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs index 8de80efd..9192c5f0 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs @@ -29,6 +29,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs index 9bd7a2d6..99dbf53e 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs @@ -24,6 +24,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor); } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs index 94405ee1..8aa04aa3 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs @@ -24,6 +24,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessJsonResponse.Descriptor); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs index 1b15e441..8f91b3e3 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs @@ -43,6 +43,7 @@ public static partial class OpenIddictServerOwinHandlers RemoveCachedRequest.Descriptor, AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, ProcessHostRedirectionResponse.Descriptor, ProcessPassthroughErrorResponse.Descriptor, diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs index 065ca209..3d6e74d0 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs @@ -29,6 +29,7 @@ public static partial class OpenIddictServerOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 0410ccbd..159a36b0 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -1046,6 +1046,73 @@ public static partial class OpenIddictServerOwinHandlers } } + /// + /// Contains the logic responsible for suppressing the redirection applied by FormsAuthenticationModule, if necessary. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class SuppressFormsAuthenticationRedirect : IOpenIddictServerHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachOwinResponseChallenge.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext 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 response = context.Transaction.GetOwinRequest()?.Context.Response ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Similarly to the automatic authentication mode used by OWIN authentication middleware, + // the ASP.NET FormsAuthentication module aggressively intercepts 401 responses even if + // the request has already been fully handled by another component (like OpenIddict). + // To prevent that, this handler is responsible for suppressing the redirection enforced + // by FormsAuthenticationModule when the status code was set to 401 (the only status code + // used by the FormsAuthenticationModule) and the OWIN application is hosted on SystemWeb. + if (response.StatusCode is 401) + { + TrySuppressFormsAuthenticationRedirect(response.Environment); + } + + return default; + + static void TrySuppressFormsAuthenticationRedirect(IDictionary environment) + { + // Note: the OWIN host cannot depend on the OWIN SystemWeb package but a direct access + // to the underlying ASP.NET 4.x context is required to be able to disable the redirection + // enforced by the FormsAuthentication module. To work around that, the HttpContextBase + // instance is resolved from the OWIN environment and SuppressFormsAuthenticationRedirect + // is set to true using a dynamic runtime resolution (that uses reflection under the hood). + if (environment.TryGetValue("System.Web.HttpContextBase", out dynamic context)) + { + try + { + // Note: the SuppressFormsAuthenticationRedirect property was introduced in ASP.NET 4.5 + // and thus should always be present, as OpenIddict requires targeting ASP.NET >= 4.6.1. + context.Response.SuppressFormsAuthenticationRedirect = true; + } + + catch + { + } + } + } + } + } + /// /// Contains the logic responsible for attaching the appropriate HTTP response cache headers. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. @@ -1059,7 +1126,7 @@ public static partial class OpenIddictServerOwinHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(AttachOwinResponseChallenge.Descriptor.Order + 1_000) + .SetOrder(SuppressFormsAuthenticationRedirect.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index 6ce97fd0..63c4f56b 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -8,8 +8,6 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.Text; -using System.Text.Encodings.Web; -using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Owin; @@ -44,12 +42,14 @@ public static partial class OpenIddictValidationOwinHandlers */ AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessChallengeErrorResponse.Descriptor, AttachHttpResponseCode.Descriptor, AttachOwinResponseChallenge.Descriptor, + SuppressFormsAuthenticationRedirect.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, ProcessChallengeErrorResponse.Descriptor, @@ -424,6 +424,73 @@ public static partial class OpenIddictValidationOwinHandlers } } + /// + /// Contains the logic responsible for suppressing the redirection applied by FormsAuthenticationModule, if necessary. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class SuppressFormsAuthenticationRedirect : IOpenIddictValidationHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachOwinResponseChallenge.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext 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 response = context.Transaction.GetOwinRequest()?.Context.Response ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Similarly to the automatic authentication mode used by OWIN authentication middleware, + // the ASP.NET FormsAuthentication module aggressively intercepts 401 responses even if + // the request has already been fully handled by another component (like OpenIddict). + // To prevent that, this handler is responsible for suppressing the redirection enforced + // by FormsAuthenticationModule when the status code was set to 401 (the only status code + // used by the FormsAuthenticationModule) and the OWIN application is hosted on SystemWeb. + if (response.StatusCode is 401) + { + TrySuppressFormsAuthenticationRedirect(response.Environment); + } + + return default; + + static void TrySuppressFormsAuthenticationRedirect(IDictionary environment) + { + // Note: the OWIN host cannot depend on the OWIN SystemWeb package but a direct access + // to the underlying ASP.NET 4.x context is required to be able to disable the redirection + // enforced by the FormsAuthentication module. To work around that, the HttpContextBase + // instance is resolved from the OWIN environment and SuppressFormsAuthenticationRedirect + // is set to true using a dynamic runtime resolution (that uses reflection under the hood). + if (environment.TryGetValue("System.Web.HttpContextBase", out dynamic context)) + { + try + { + // Note: the SuppressFormsAuthenticationRedirect property was introduced in ASP.NET 4.5 + // and thus should always be present, as OpenIddict requires targeting ASP.NET >= 4.6.1. + context.Response.SuppressFormsAuthenticationRedirect = true; + } + + catch + { + } + } + } + } + } + /// /// Contains the logic responsible for attaching the appropriate HTTP response cache headers. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. @@ -437,7 +504,7 @@ public static partial class OpenIddictValidationOwinHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(AttachOwinResponseChallenge.Descriptor.Order + 1_000) + .SetOrder(SuppressFormsAuthenticationRedirect.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build();