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.