From 8546ca47affeb3331d0822baaa1c6ba00e319d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 19 Aug 2019 19:26:13 +0200 Subject: [PATCH] Bring back logout/end session endpoint support --- samples/Mvc.Server/Startup.cs | 2 +- .../OpenIddictServerAspNetCoreBuilder.cs | 10 + ...OpenIddictServerAspNetCoreConfiguration.cs | 2 +- .../OpenIddictServerAspNetCoreExtensions.cs | 1 + ...penIddictServerAspNetCoreHandlerFilters.cs | 21 + ...ServerAspNetCoreHandlers.Authentication.cs | 2 +- ...IddictServerAspNetCoreHandlers.Exchange.cs | 1 - ...nIddictServerAspNetCoreHandlers.Session.cs | 659 ++++++++++++++++++ .../OpenIddictServerAspNetCoreHandlers.cs | 3 +- .../OpenIddictServerAspNetCoreOptions.cs | 8 + .../OpenIddictServerOwinBuilder.cs | 10 + .../OpenIddictServerOwinExtensions.cs | 1 + .../OpenIddictServerOwinHandlerFilters.cs | 22 + ...IddictServerOwinHandlers.Authentication.cs | 2 +- .../OpenIddictServerOwinHandlers.Exchange.cs | 1 - .../OpenIddictServerOwinHandlers.Session.cs | 587 ++++++++++++++++ .../OpenIddictServerOwinHandlers.cs | 3 +- .../OpenIddictServerOwinHelpers.cs | 1 - .../OpenIddictServerOwinOptions.cs | 8 + .../OpenIddictServerConfiguration.cs | 11 + .../OpenIddictServerEvents.Exchange.cs | 3 - .../OpenIddictServerEvents.Serialization.cs | 2 - .../OpenIddictServerEvents.Session.cs | 10 + .../OpenIddictServerExtensions.cs | 1 + .../OpenIddictServerHandlerFilters.cs | 16 + .../OpenIddictServerHandlers.Session.cs | 571 +++++++++++++++ .../OpenIddictServerHandlers.cs | 3 +- 27 files changed, 1946 insertions(+), 15 deletions(-) create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index f4f02066..4ce5175c 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -89,6 +88,7 @@ namespace Mvc.Server options.UseAspNetCore() .EnableStatusCodePagesIntegration() .EnableAuthorizationEndpointPassthrough() + .EnableLogoutEndpointPassthrough() .EnableTokenEndpointPassthrough() .EnableUserinfoEndpointPassthrough() .DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs index 51078ed8..22b6cf07 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs @@ -79,6 +79,16 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerAspNetCoreBuilder EnableErrorPassthrough() => Configure(options => options.EnableErrorPassthrough = true); + /// + /// Enables the pass-through mode for the OpenID Connect logout endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableLogoutEndpointPassthrough() + => Configure(options => options.EnableLogoutEndpointPassthrough = true); + /// /// Enables the pass-through mode for the OpenID Connect token endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs index 049f14ff..338d3ec5 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs @@ -93,7 +93,7 @@ namespace OpenIddict.Server.AspNetCore .AppendLine("The OpenIddict ASP.NET Core server cannot be used as the default scheme handler.") .Append("Make sure that neither DefaultAuthenticateScheme, DefaultChallengeScheme, ") .Append("DefaultForbidScheme, DefaultSignInScheme, DefaultSignOutScheme nor DefaultScheme ") - .Append("point to an instance of the OpenIddict server handler.") + .Append("point to an instance of the OpenIddict ASP.NET Core server handler.") .ToString()); } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs index 0e045722..67702538 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs @@ -47,6 +47,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs index c0402351..8cda8b75 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs @@ -78,6 +78,27 @@ namespace OpenIddict.Server.AspNetCore return Task.FromResult(context.Transaction.GetHttpRequest() != null); } } + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the logout endpoint. + /// + public class RequireLogoutEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireLogoutEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableLogoutEndpointPassthrough); + } + } /// /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs index 3488a369..53a60987 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs @@ -28,7 +28,6 @@ using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants; using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; using static OpenIddict.Server.OpenIddictServerEvents; -using static OpenIddict.Server.OpenIddictServerHandlers; namespace OpenIddict.Server.AspNetCore { @@ -55,6 +54,7 @@ namespace OpenIddict.Server.AspNetCore RemoveCachedRequest.Descriptor, ProcessFormPostResponse.Descriptor, ProcessQueryResponse.Descriptor, + ProcessFragmentResponse.Descriptor, ProcessStatusCodePagesErrorResponse.Descriptor, ProcessPassthroughErrorResponse.Descriptor, ProcessLocalErrorResponse.Descriptor); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs index e4bc0e68..14d312e8 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs @@ -11,7 +11,6 @@ using JetBrains.Annotations; using Microsoft.AspNetCore; using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; using static OpenIddict.Server.OpenIddictServerEvents; -using static OpenIddict.Server.OpenIddictServerHandlers; namespace OpenIddict.Server.AspNetCore { diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs new file mode 100644 index 00000000..efcdf6a4 --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs @@ -0,0 +1,659 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Session + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Logout request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + RestoreCachedRequestParameters.Descriptor, + CacheRequestParameters.Descriptor, + + /* + * Logout request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Logout response processing: + */ + RemoveCachedRequest.Descriptor, + ProcessEmptyResponse.Descriptor, + ProcessQueryResponse.Descriptor, + ProcessStatusCodePagesErrorResponse.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of restoring cached requests from the request_id, if specified. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class RestoreCachedRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RestoreCachedRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RestoreCachedRequestParameters([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractLogoutRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a request_id parameter can be found in the logout request, + // restore the complete logout request from the distributed cache. + + if (string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + var payload = await _cache.GetAsync(Cache.LogoutRequest + context.Request.RequestId); + if (payload == null) + { + context.Logger.LogError("The logout request was rejected because an unknown " + + "or invalid request_id parameter was specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'request_id' parameter is invalid."); + + return; + } + + // Restore the logout request parameters from the serialized payload. + using var reader = new BsonDataReader(new MemoryStream(payload)); + foreach (var parameter in JObject.Load(reader)) + { + // Avoid overriding the current request parameters. + if (context.Request.HasParameter(parameter.Key)) + { + continue; + } + + context.Request.SetParameter(parameter.Key, parameter.Value); + } + } + } + + /// + /// Contains the logic responsible of caching logout requests, if applicable. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class CacheRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + private readonly IOptionsMonitor _options; + + public CacheRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public CacheRequestParameters( + [NotNull] IDistributedCache cache, + [NotNull] IOptionsMonitor options) + { + _cache = cache; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(RestoreCachedRequestParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractLogoutRequestContext context) + { + if (context == 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(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // Don't cache the request if the request doesn't include any parameter. + // If a request_id parameter can be found in the logout request, + // ignore the following logic to prevent an infinite redirect loop. + if (context.Request.GetParameters().IsEmpty || !string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Generate a 256-bit request identifier using a crypto-secure random number generator. + var data = new byte[256 / 8]; + RandomNumberGenerator.Fill(data); + + context.Request.RequestId = Base64UrlEncoder.Encode(data); + + // Store the serialized logout request parameters in the distributed cache. + var stream = new MemoryStream(); + using (var writer = new BsonDataWriter(stream)) + { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, context.Request); + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + await _cache.SetAsync(Cache.LogoutRequest + context.Request.RequestId, + stream.ToArray(), _options.CurrentValue.RequestCachingPolicy); + + // Create a new GET logout request containing only the request_id parameter. + var address = QueryHelpers.AddQueryString( + uri: request.Scheme + "://" + request.Host + request.PathBase + request.Path, + name: Parameters.RequestId, + value: context.Request.RequestId); + + request.HttpContext.Response.Redirect(address); + + // Mark the response as handled to skip the rest of the pipeline. + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleLogoutRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of removing cached logout requests from the distributed cache. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class RemoveCachedRequest : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RemoveCachedRequest() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RemoveCachedRequest([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessQueryResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Request?.RequestId)) + { + return Task.CompletedTask; + } + + // Note: the ApplyLogoutResponse event is called for both successful + // and errored logout responses but discrimination is not necessary here, + // as the logout request must be removed from the distributed cache in both cases. + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + return _cache.RemoveAsync(Cache.LogoutRequest + context.Request.RequestId); + } + } + + /// + /// Contains the logic responsible of processing logout responses that don't specify a post_logout_redirect_uri. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessEmptyResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessPassthroughErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The logout response was successfully returned: {Response}.", response); + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessQueryResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessEmptyResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The logout response was successfully returned to '{PostLogoutRedirectUri}': {Response}.", + context.PostLogoutRedirectUri, response); + + var location = context.PostLogoutRedirectUri; + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + location = QueryHelpers.AddQueryString(location, parameter.Key, parameter.Value); + } + + response.Redirect(location); + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses that must be handled by another + /// middleware in the pipeline at a later stage (e.g an ASP.NET Core MVC action or a NancyFX module). + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessPassthroughErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessStatusCodePagesErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses handled by the status code pages middleware. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessStatusCodePagesErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Response == null) + { + throw new InvalidOperationException("This handler cannot be invoked without a response attached."); + } + + // 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Error) || !string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + // Determine if the status code pages middleware has been enabled for this request. + // If it was not registered or enabled, let the default OpenIddict server handlers render + // a default error page instead of delegating the rendering to the status code middleware. + var feature = response.HttpContext.Features.Get(); + if (feature == null || !feature.Enabled) + { + return Task.CompletedTask; + } + + // Replace the default status code to return a 400 response. + response.StatusCode = 400; + + // Mark the request as fully handled to prevent the other OpenIddict server handlers + // from displaying the default error page and to allow the status code pages middleware + // to rewrite the response using the logic defined by the developer when registering it. + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses that must be returned as plain-text. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessLocalErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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 response = context.Transaction.GetHttpRequest()?.HttpContext.Response; + if (response == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return; + } + + // Don't return the state originally sent by the client application. + context.Response.State = null; + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.Logger.LogInformation("The logout response was successfully returned " + + "as a plain-text document: {Response}.", context.Response); + + using (var buffer = new MemoryStream()) + using (var writer = new StreamWriter(buffer)) + { + foreach (var parameter in context.Response.GetParameters()) + { + // Ignore null or empty parameters, including JSON + // objects that can't be represented as strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) + { + continue; + } + + writer.WriteLine("{0}:{1}", parameter.Key, value); + } + + writer.Flush(); + + response.ContentLength = buffer.Length; + response.ContentType = "text/plain;charset=UTF-8"; + + response.Headers[HeaderNames.CacheControl] = "no-cache"; + response.Headers[HeaderNames.Pragma] = "no-cache"; + response.Headers[HeaderNames.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096, response.HttpContext.RequestAborted); + } + + context.HandleRequest(); + } + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 026cbfa3..689154de 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -37,7 +37,8 @@ namespace OpenIddict.Server.AspNetCore .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) - .AddRange(Serialization.DefaultHandlers); + .AddRange(Serialization.DefaultHandlers) + .AddRange(Session.DefaultHandlers); /// /// Contains the logic responsible of inferring the endpoint type from the request address. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs index a3c5fd35..f1d2e21a 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs @@ -39,6 +39,14 @@ namespace OpenIddict.Server.AspNetCore /// public bool EnableErrorPassthrough { get; set; } + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the logout endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableLogoutEndpointPassthrough { get; set; } + /// /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the token endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs index b55c05fc..03cb03f0 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs @@ -79,6 +79,16 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerOwinBuilder EnableErrorPassthrough() => Configure(options => options.EnableErrorPassthrough = true); + /// + /// Enables the pass-through mode for the OpenID Connect logout endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + /// The . + public OpenIddictServerOwinBuilder EnableLogoutEndpointPassthrough() + => Configure(options => options.EnableLogoutEndpointPassthrough = true); + /// /// Enables the pass-through mode for the OpenID Connect token endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs index f6b5526e..50853fa3 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs @@ -48,6 +48,7 @@ namespace Microsoft.Extensions.DependencyInjection // Register the built-in filters used by the default OpenIddict OWIN server event handlers. builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs index 2c755e47..efcbc96f 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs @@ -61,6 +61,28 @@ namespace OpenIddict.Server.Owin } } + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the logout endpoint. + /// + public class RequireLogoutEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireLogoutEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(_options.CurrentValue.EnableLogoutEndpointPassthrough); + } + } + /// /// Represents a filter that excludes the associated handlers if no OWIN request can be found. /// diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs index 6a38691c..2bff3200 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs @@ -24,7 +24,6 @@ using OpenIddict.Abstractions; using Owin; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; -using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants; using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; @@ -53,6 +52,7 @@ namespace OpenIddict.Server.Owin RemoveCachedRequest.Descriptor, ProcessFormPostResponse.Descriptor, ProcessQueryResponse.Descriptor, + ProcessFragmentResponse.Descriptor, ProcessPassthroughErrorResponse.Descriptor, ProcessLocalErrorResponse.Descriptor); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs index 8b4b1a6f..90e68b35 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs @@ -9,7 +9,6 @@ using System.Collections.Immutable; using System.Threading.Tasks; using JetBrains.Annotations; using static OpenIddict.Server.OpenIddictServerEvents; -using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; namespace OpenIddict.Server.Owin diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs new file mode 100644 index 00000000..7a6d1606 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs @@ -0,0 +1,587 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Owin.Infrastructure; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using Owin; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Session + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Logout request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + RestoreCachedRequestParameters.Descriptor, + CacheRequestParameters.Descriptor, + + /* + * Logout request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Logout response processing: + */ + RemoveCachedRequest.Descriptor, + ProcessEmptyResponse.Descriptor, + ProcessQueryResponse.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of restoring cached requests from the request_id, if specified. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class RestoreCachedRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RestoreCachedRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RestoreCachedRequestParameters([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtractGetOrPostRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractLogoutRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a request_id parameter can be found in the logout request, + // restore the complete logout request from the distributed cache. + + if (string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + var payload = await _cache.GetAsync(Cache.LogoutRequest + context.Request.RequestId); + if (payload == null) + { + context.Logger.LogError("The logout request was rejected because an unknown " + + "or invalid request_id parameter was specified."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'request_id' parameter is invalid."); + + return; + } + + // Restore the logout request parameters from the serialized payload. + using var reader = new BsonDataReader(new MemoryStream(payload)); + foreach (var parameter in JObject.Load(reader)) + { + // Avoid overriding the current request parameters. + if (context.Request.HasParameter(parameter.Key)) + { + continue; + } + + context.Request.SetParameter(parameter.Key, parameter.Value); + } + } + } + + /// + /// Contains the logic responsible of caching logout requests, if applicable. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class CacheRequestParameters : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + private readonly IOptionsMonitor _options; + + public CacheRequestParameters() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public CacheRequestParameters( + [NotNull] IDistributedCache cache, + [NotNull] IOptionsMonitor options) + { + _cache = cache; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(RestoreCachedRequestParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ExtractLogoutRequestContext context) + { + if (context == 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(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // Don't cache the request if the request doesn't include any parameter. + // If a request_id parameter can be found in the logout request, + // ignore the following logic to prevent an infinite redirect loop. + if (context.Request.GetParameters().IsEmpty || !string.IsNullOrEmpty(context.Request.RequestId)) + { + return; + } + + // Generate a 256-bit request identifier using a crypto-secure random number generator. + var data = new byte[256 / 8]; + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); + + context.Request.RequestId = Base64UrlEncoder.Encode(data); + + // Store the serialized logout request parameters in the distributed cache. + var stream = new MemoryStream(); + using (var writer = new BsonDataWriter(stream)) + { + writer.CloseOutput = false; + + var serializer = JsonSerializer.CreateDefault(); + serializer.Serialize(writer, context.Request); + } + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + await _cache.SetAsync(Cache.LogoutRequest + context.Request.RequestId, + stream.ToArray(), _options.CurrentValue.RequestCachingPolicy); + + // Create a new GET logout request containing only the request_id parameter. + var address = WebUtilities.AddQueryString( + uri: request.Scheme + "://" + request.Host + request.PathBase + request.Path, + name: Parameters.RequestId, + value: context.Request.RequestId); + + request.Context.Response.Redirect(address); + + // Mark the response as handled to skip the rest of the pipeline. + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] HandleLogoutRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of removing cached logout requests from the distributed cache. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class RemoveCachedRequest : IOpenIddictServerHandler + { + private readonly IDistributedCache _cache; + + public RemoveCachedRequest() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("A distributed cache instance must be registered when enabling request caching.") + .Append("To register the default in-memory distributed cache implementation, reference the ") + .Append("'Microsoft.Extensions.Caching.Memory' package and call ") + .Append("'services.AddDistributedMemoryCache()' from 'ConfigureServices'.") + .ToString()); + + public RemoveCachedRequest([NotNull] IDistributedCache cache) + => _cache = cache; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessQueryResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Request?.RequestId)) + { + return Task.CompletedTask; + } + + // Note: the ApplyLogoutResponse event is called for both successful + // and errored logout responses but discrimination is not necessary here, + // as the logout request must be removed from the distributed cache in both cases. + + // Note: the cache key is always prefixed with a specific marker + // to avoid collisions with the other types of cached payloads. + return _cache.RemoveAsync(Cache.LogoutRequest + context.Request.RequestId); + } + } + + /// + /// Contains the logic responsible of processing logout responses that don't specify a post_logout_redirect_uri. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessEmptyResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessPassthroughErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The logout response was successfully returned: {Response}.", response); + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessQueryResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessEmptyResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + context.Logger.LogInformation("The logout response was successfully returned to '{PostLogoutRedirectUri}': {Response}.", + context.PostLogoutRedirectUri, response); + + var location = context.PostLogoutRedirectUri; + + // Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters + // with the same name are used by derived drafts like the OAuth 2.0 token exchange specification. + // For consistency, multiple parameters with the same name are also supported by this endpoint. + foreach (var parameter in context.Response.GetFlattenedParameters()) + { + location = WebUtilities.AddQueryString(location, parameter.Key, parameter.Value); + } + + response.Redirect(location); + context.HandleRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses that must be handled by another + /// middleware in the pipeline at a later stage (e.g an OWIN MVC action or a NancyFX module). + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessPassthroughErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.SkipRequest(); + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of processing logout responses that must be returned as plain-text. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessLocalErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == 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; + if (response == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + if (string.IsNullOrEmpty(context.Response.Error) || !string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return; + } + + // Don't return the state originally sent by the client application. + context.Response.State = null; + + // Apply a 400 status code by default. + response.StatusCode = 400; + + context.Logger.LogInformation("The logout response was successfully returned " + + "as a plain-text document: {Response}.", context.Response); + + using (var buffer = new MemoryStream()) + using (var writer = new StreamWriter(buffer)) + { + foreach (var parameter in context.Response.GetParameters()) + { + // Ignore null or empty parameters, including JSON + // objects that can't be represented as strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) + { + continue; + } + + writer.WriteLine("{0}:{1}", parameter.Key, value); + } + + writer.Flush(); + + response.ContentLength = buffer.Length; + response.ContentType = "text/plain;charset=UTF-8"; + + response.Headers["Cache-Control"] = "no-cache"; + response.Headers["Pragma"] = "no-cache"; + response.Headers["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"; + + buffer.Seek(offset: 0, loc: SeekOrigin.Begin); + await buffer.CopyToAsync(response.Body, 4096, response.Context.Request.CallCancelled); + } + + context.HandleRequest(); + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 4cad76bb..dcbe2709 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -36,7 +36,8 @@ namespace OpenIddict.Server.Owin .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) - .AddRange(Serialization.DefaultHandlers); + .AddRange(Serialization.DefaultHandlers) + .AddRange(Session.DefaultHandlers); /// /// Contains the logic responsible of inferring the endpoint type from the request address. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs index e9255dfb..87a4101e 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs @@ -5,7 +5,6 @@ */ using System; -using System.ComponentModel; using JetBrains.Annotations; using Microsoft.Owin; using OpenIddict.Abstractions; diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs index 11c60e30..77a040ba 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs @@ -46,6 +46,14 @@ namespace OpenIddict.Server.Owin /// public bool EnableErrorPassthrough { get; set; } + /// + /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the authorization endpoint. + /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. + /// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests + /// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance). + /// + public bool EnableLogoutEndpointPassthrough { get; set; } + /// /// Gets or sets a boolean indicating whether the pass-through mode is enabled for the token endpoint. /// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict. diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index 98407a56..37c2619e 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -116,6 +116,17 @@ namespace OpenIddict.Server .ToString()); } + if (options.LogoutEndpointUris.Count != 0 && !options.CustomHandlers.Any( + descriptor => descriptor.ContextType == typeof(ValidateLogoutRequestContext) && + descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No custom logout request validation handler was found. When enabling the degraded mode, ") + .Append("a custom 'IOpenIddictServerHandler' must be implemented ") + .Append("to validate logout requests (e.g to ensure the post_logout_redirect_uri is valid).") + .ToString()); + } + if (options.TokenEndpointUris.Count != 0 && !options.CustomHandlers.Any( descriptor => descriptor.ContextType == typeof(ValidateTokenRequestContext) && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs index c136383a..8c4460d1 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs @@ -4,9 +4,6 @@ * the license and the contributors participating to this project. */ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Security.Claims; using JetBrains.Annotations; diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs index bbd749fa..3385831f 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs @@ -5,8 +5,6 @@ */ using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Security.Claims; using JetBrains.Annotations; using Microsoft.IdentityModel.JsonWebTokens; diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs index dd1ec80d..2682256b 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs @@ -83,6 +83,16 @@ namespace OpenIddict.Server : base(transaction) { } + + /// + /// Gets a boolean indicating whether the logout request should be processed. + /// + public bool IsLogoutAllowed { get; private set; } + + /// + /// Allow the logout request to be processed. + /// + public void ProcessLogout() => IsLogoutAllowed = true; } /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index e1a05a6c..32004ed0 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -51,6 +51,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index ff804416..0c759c28 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -143,6 +143,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers when no post_logout_redirect_uri is received. + /// + public class RequirePostLogoutRedirectUriParameter : IOpenIddictServerHandlerFilter + { + public Task IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return Task.FromResult(!string.IsNullOrEmpty(context.Request.PostLogoutRedirectUri)); + } + } + /// /// Represents a filter that excludes the associated handlers if no refresh token is returned. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs new file mode 100644 index 00000000..8c579ee6 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs @@ -0,0 +1,571 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Session + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Logout request top-level processing: + */ + ExtractLogoutRequest.Descriptor, + ValidateLogoutRequest.Descriptor, + HandleLogoutRequest.Descriptor, + ApplyLogoutResponse.Descriptor, + ApplyLogoutResponse.Descriptor, + ApplyLogoutResponse.Descriptor, + + /* + * Logout request validation: + */ + ValidatePostLogoutRedirectUriParameter.Descriptor, + ValidateClientPostLogoutRedirectUri.Descriptor, + + /* + * Logout response processing: + */ + AttachPostLogoutRedirectUri.Descriptor, + AttachResponseState.Descriptor); + + /// + /// Contains the logic responsible of extracting logout requests and invoking the corresponding event handlers. + /// + public class ExtractLogoutRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractLogoutRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Logout) + { + return; + } + + var notification = new ExtractLogoutRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The logout request was not correctly extracted. To extract logout requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The logout request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating logout requests and invoking the corresponding event handlers. + /// + public class ValidateLogoutRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateLogoutRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractLogoutRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Logout) + { + return; + } + + var notification = new ValidateLogoutRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (!string.IsNullOrEmpty(notification.PostLogoutRedirectUri)) + { + // Store the validated post_logout_redirect_uri as an environment property. + context.Transaction.Properties[Properties.PostLogoutRedirectUri] = notification.PostLogoutRedirectUri; + } + + context.Logger.LogInformation("The logout request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling logout requests and invoking the corresponding event handlers. + /// + public class HandleLogoutRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleLogoutRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateLogoutRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Logout) + { + return; + } + + var notification = new HandleLogoutRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.IsLogoutAllowed) + { + var @event = new ProcessSignoutResponseContext(context.Transaction) + { + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(@event); + + if (@event.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (@event.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The logout request was not handled. To handle logout requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .Append("Alternatively, enable the pass-through mode to handle them at a later stage.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyLogoutResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyLogoutResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Logout) + { + return; + } + + var notification = new ApplyLogoutResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting logout requests that specify an invalid post_logout_redirect_uri parameter. + /// + public class ValidatePostLogoutRedirectUriParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ValidateLogoutRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return Task.CompletedTask; + } + + // If an optional post_logout_redirect_uri was provided, validate it. + if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri uri) || + !uri.IsWellFormedOriginalString()) + { + context.Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " + + "was not a valid absolute URL: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'post_logout_redirect_uri' parameter must be a valid absolute URL."); + + return Task.CompletedTask; + } + + if (!string.IsNullOrEmpty(uri.Fragment)) + { + context.Logger.LogError("The logout request was rejected because the 'post_logout_redirect_uri' contained " + + "a URL fragment: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'post_logout_redirect_uri' parameter must not include a fragment."); + + return Task.CompletedTask; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of rejecting logout requests that use an invalid redirect_uri. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientPostLogoutRedirectUri : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientPostLogoutRedirectUri() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientPostLogoutRedirectUri([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task HandleAsync([NotNull] ValidateLogoutRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + async Task ValidatePostLogoutRedirectUriAsync(string address) + { + var applications = await _applicationManager.FindByPostLogoutRedirectUriAsync(address); + if (applications.IsDefaultOrEmpty) + { + return false; + } + + if (context.Options.IgnoreEndpointPermissions) + { + return true; + } + + foreach (var application in applications) + { + if (await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Logout)) + { + return true; + } + } + + return false; + } + + if (!await ValidatePostLogoutRedirectUriAsync(context.PostLogoutRedirectUri)) + { + context.Logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " + + "was unknown: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri); + + context.Reject( + error: Errors.InvalidRequest, + description: "The specified 'post_logout_redirect_uri' parameter is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of inferring the redirect URL + /// used to send the response back to the client application. + /// + public class AttachPostLogoutRedirectUri : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Request == null) + { + return Task.CompletedTask; + } + + // Note: at this stage, the validated redirect URI property may be null (e.g if an error + // is returned from the ExtractLogoutRequest/ValidateLogoutRequest events). + if (context.Transaction.Properties.TryGetValue(Properties.PostLogoutRedirectUri, out var property)) + { + context.PostLogoutRedirectUri = (string) property; + } + + return Task.CompletedTask; + } + } + + /// + /// Contains the logic responsible of attaching the state to the response. + /// + public class AttachResponseState : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachPostLogoutRedirectUri.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync([NotNull] ApplyLogoutResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Attach the request state to the logout response. + if (string.IsNullOrEmpty(context.Response.State)) + { + context.Response.State = context.Request?.State; + } + + return Task.CompletedTask; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 77d82627..d421a619 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -45,7 +45,8 @@ namespace OpenIddict.Server .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) - .AddRange(Serialization.DefaultHandlers); + .AddRange(Serialization.DefaultHandlers) + .AddRange(Session.DefaultHandlers); /// /// Contains the logic responsible of ensuring that the challenge response contains an appropriate error.