Browse Source

Bring back logout/end session endpoint support

pull/775/head
Kévin Chalet 7 years ago
committed by GitHub
parent
commit
8546ca47af
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/Mvc.Server/Startup.cs
  2. 10
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
  3. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
  4. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
  5. 21
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs
  6. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  7. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
  8. 659
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  9. 3
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  10. 8
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs
  11. 10
      src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
  12. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
  13. 22
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs
  14. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  15. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
  16. 587
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  17. 3
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  18. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs
  19. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs
  20. 11
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  21. 3
      src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs
  22. 2
      src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs
  23. 10
      src/OpenIddict.Server/OpenIddictServerEvents.Session.cs
  24. 1
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  25. 16
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  26. 571
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  27. 3
      src/OpenIddict.Server/OpenIddictServerHandlers.cs

2
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.

10
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs

@ -79,6 +79,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerAspNetCoreBuilder EnableErrorPassthrough()
=> Configure(options => options.EnableErrorPassthrough = true);
/// <summary>
/// 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).
/// </summary>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/>.</returns>
public OpenIddictServerAspNetCoreBuilder EnableLogoutEndpointPassthrough()
=> Configure(options => options.EnableLogoutEndpointPassthrough = true);
/// <summary>
/// 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.

2
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());
}
}

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs

@ -47,6 +47,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireAuthorizationEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireErrorPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireHttpRequest>();
builder.Services.TryAddSingleton<RequireLogoutEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireTransportSecurityRequirementEnabled>();
builder.Services.TryAddSingleton<RequireRequestCachingEnabled>();
builder.Services.TryAddSingleton<RequireStatusCodePagesIntegrationEnabled>();

21
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs

@ -78,6 +78,27 @@ namespace OpenIddict.Server.AspNetCore
return Task.FromResult(context.Transaction.GetHttpRequest() != null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the logout endpoint.
/// </summary>
public class RequireLogoutEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;
public RequireLogoutEndpointPassthroughEnabled([NotNull] IOptionsMonitor<OpenIddictServerAspNetCoreOptions> options)
=> _options = options;
public Task<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return Task.FromResult(_options.CurrentValue.EnableLogoutEndpointPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled.

2
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);

1
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
{

659
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Logout request extraction:
*/
ExtractGetOrPostRequest<ExtractLogoutRequestContext>.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);
/// <summary>
/// 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.
/// </summary>
public class RestoreCachedRequestParameters : IOpenIddictServerHandler<ExtractLogoutRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ExtractLogoutRequestContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireRequestCachingEnabled>()
.UseSingletonHandler<RestoreCachedRequestParameters>()
.SetOrder(ExtractGetOrPostRequest<ExtractLogoutRequestContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
public class CacheRequestParameters : IOpenIddictServerHandler<ExtractLogoutRequestContext>
{
private readonly IDistributedCache _cache;
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _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<OpenIddictServerAspNetCoreOptions> options)
{
_cache = cache;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ExtractLogoutRequestContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireRequestCachingEnabled>()
.UseSingletonHandler<CacheRequestParameters>()
.SetOrder(RestoreCachedRequestParameters.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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();
}
}
/// <summary>
/// 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.
/// </summary>
public class EnablePassthroughMode : IOpenIddictServerHandler<HandleLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleLogoutRequestContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireLogoutEndpointPassthroughEnabled>()
.UseSingletonHandler<EnablePassthroughMode>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public Task HandleAsync([NotNull] HandleLogoutRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SkipRequest();
return Task.CompletedTask;
}
}
/// <summary>
/// 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.
/// </summary>
public class RemoveCachedRequest : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireRequestCachingEnabled>()
.UseSingletonHandler<RemoveCachedRequest>()
.SetOrder(ProcessQueryResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessEmptyResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessQueryResponse>()
.SetOrder(ProcessPassthroughErrorResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessQueryResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessQueryResponse>()
.SetOrder(ProcessEmptyResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessPassthroughErrorResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireErrorPassthroughEnabled>()
.AddFilter<RequireLogoutEndpointPassthroughEnabled>()
.UseSingletonHandler<ProcessPassthroughErrorResponse>()
.SetOrder(ProcessStatusCodePagesErrorResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessStatusCodePagesErrorResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStatusCodePagesIntegrationEnabled>()
.UseSingletonHandler<ProcessStatusCodePagesErrorResponse>()
.SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<IStatusCodePagesFeature>();
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessLocalErrorResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessLocalErrorResponse>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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();
}
}
}
}
}

3
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);
/// <summary>
/// Contains the logic responsible of inferring the endpoint type from the request address.

8
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs

@ -39,6 +39,14 @@ namespace OpenIddict.Server.AspNetCore
/// </summary>
public bool EnableErrorPassthrough { get; set; }
/// <summary>
/// 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).
/// </summary>
public bool EnableLogoutEndpointPassthrough { get; set; }
/// <summary>
/// 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.

10
src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs

@ -79,6 +79,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerOwinBuilder EnableErrorPassthrough()
=> Configure(options => options.EnableErrorPassthrough = true);
/// <summary>
/// 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).
/// </summary>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/>.</returns>
public OpenIddictServerOwinBuilder EnableLogoutEndpointPassthrough()
=> Configure(options => options.EnableLogoutEndpointPassthrough = true);
/// <summary>
/// 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.

1
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<RequireAuthorizationEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireErrorPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireLogoutEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireTransportSecurityRequirementEnabled>();
builder.Services.TryAddSingleton<RequireOwinRequest>();
builder.Services.TryAddSingleton<RequireRequestCachingEnabled>();

22
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs

@ -61,6 +61,28 @@ namespace OpenIddict.Server.Owin
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the logout endpoint.
/// </summary>
public class RequireLogoutEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;
public RequireLogoutEndpointPassthroughEnabled([NotNull] IOptionsMonitor<OpenIddictServerOwinOptions> options)
=> _options = options;
public Task<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return Task.FromResult(_options.CurrentValue.EnableLogoutEndpointPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no OWIN request can be found.
/// </summary>

2
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);

1
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

587
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Logout request extraction:
*/
ExtractGetOrPostRequest<ExtractLogoutRequestContext>.Descriptor,
RestoreCachedRequestParameters.Descriptor,
CacheRequestParameters.Descriptor,
/*
* Logout request handling:
*/
EnablePassthroughMode.Descriptor,
/*
* Logout response processing:
*/
RemoveCachedRequest.Descriptor,
ProcessEmptyResponse.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessPassthroughErrorResponse.Descriptor,
ProcessLocalErrorResponse.Descriptor);
/// <summary>
/// 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.
/// </summary>
public class RestoreCachedRequestParameters : IOpenIddictServerHandler<ExtractLogoutRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ExtractLogoutRequestContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireRequestCachingEnabled>()
.UseSingletonHandler<RestoreCachedRequestParameters>()
.SetOrder(ExtractGetOrPostRequest<ExtractLogoutRequestContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
public class CacheRequestParameters : IOpenIddictServerHandler<ExtractLogoutRequestContext>
{
private readonly IDistributedCache _cache;
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _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<OpenIddictServerOwinOptions> options)
{
_cache = cache;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ExtractLogoutRequestContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireRequestCachingEnabled>()
.UseSingletonHandler<CacheRequestParameters>()
.SetOrder(RestoreCachedRequestParameters.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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();
}
}
/// <summary>
/// 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.
/// </summary>
public class EnablePassthroughMode : IOpenIddictServerHandler<HandleLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleLogoutRequestContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireLogoutEndpointPassthroughEnabled>()
.UseSingletonHandler<EnablePassthroughMode>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public Task HandleAsync([NotNull] HandleLogoutRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SkipRequest();
return Task.CompletedTask;
}
}
/// <summary>
/// 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.
/// </summary>
public class RemoveCachedRequest : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireRequestCachingEnabled>()
.UseSingletonHandler<RemoveCachedRequest>()
.SetOrder(ProcessQueryResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessEmptyResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessQueryResponse>()
.SetOrder(ProcessPassthroughErrorResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessQueryResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessQueryResponse>()
.SetOrder(ProcessEmptyResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessPassthroughErrorResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireErrorPassthroughEnabled>()
.AddFilter<RequireLogoutEndpointPassthroughEnabled>()
.UseSingletonHandler<ProcessPassthroughErrorResponse>()
.SetOrder(ProcessLocalErrorResponse.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessLocalErrorResponse : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessLocalErrorResponse>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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();
}
}
}
}
}

3
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);
/// <summary>
/// Contains the logic responsible of inferring the endpoint type from the request address.

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs

@ -5,7 +5,6 @@
*/
using System;
using System.ComponentModel;
using JetBrains.Annotations;
using Microsoft.Owin;
using OpenIddict.Abstractions;

8
src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs

@ -46,6 +46,14 @@ namespace OpenIddict.Server.Owin
/// </summary>
public bool EnableErrorPassthrough { get; set; }
/// <summary>
/// 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).
/// </summary>
public bool EnableLogoutEndpointPassthrough { get; set; }
/// <summary>
/// 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.

11
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<ValidateLogoutRequestContext>' 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))))

3
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;

2
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;

10
src/OpenIddict.Server/OpenIddictServerEvents.Session.cs

@ -83,6 +83,16 @@ namespace OpenIddict.Server
: base(transaction)
{
}
/// <summary>
/// Gets a boolean indicating whether the logout request should be processed.
/// </summary>
public bool IsLogoutAllowed { get; private set; }
/// <summary>
/// Allow the logout request to be processed.
/// </summary>
public void ProcessLogout() => IsLogoutAllowed = true;
}
/// <summary>

1
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -51,6 +51,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireEndpointPermissionsEnabled>();
builder.Services.TryAddSingleton<RequireGrantTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireIdentityTokenIncluded>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectUriParameter>();
builder.Services.TryAddSingleton<RequireRefreshTokenIncluded>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();

16
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -143,6 +143,22 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers when no post_logout_redirect_uri is received.
/// </summary>
public class RequirePostLogoutRedirectUriParameter : IOpenIddictServerHandlerFilter<BaseContext>
{
public Task<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return Task.FromResult(!string.IsNullOrEmpty(context.Request.PostLogoutRedirectUri));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no refresh token is returned.
/// </summary>

571
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Logout request top-level processing:
*/
ExtractLogoutRequest.Descriptor,
ValidateLogoutRequest.Descriptor,
HandleLogoutRequest.Descriptor,
ApplyLogoutResponse<ProcessErrorResponseContext>.Descriptor,
ApplyLogoutResponse<ProcessRequestContext>.Descriptor,
ApplyLogoutResponse<ProcessSignoutResponseContext>.Descriptor,
/*
* Logout request validation:
*/
ValidatePostLogoutRedirectUriParameter.Descriptor,
ValidateClientPostLogoutRedirectUri.Descriptor,
/*
* Logout response processing:
*/
AttachPostLogoutRedirectUri.Descriptor,
AttachResponseState.Descriptor);
/// <summary>
/// Contains the logic responsible of extracting logout requests and invoking the corresponding event handlers.
/// </summary>
public class ExtractLogoutRequest : IOpenIddictServerHandler<ProcessRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ExtractLogoutRequest([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseScopedHandler<ExtractLogoutRequest>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<ExtractLogoutRequestContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
context.Logger.LogInformation("The logout request was successfully extracted: {Request}.", notification.Request);
}
}
/// <summary>
/// Contains the logic responsible of validating logout requests and invoking the corresponding event handlers.
/// </summary>
public class ValidateLogoutRequest : IOpenIddictServerHandler<ProcessRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateLogoutRequest([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseScopedHandler<ValidateLogoutRequest>()
.SetOrder(ExtractLogoutRequest.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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.");
}
}
/// <summary>
/// Contains the logic responsible of handling logout requests and invoking the corresponding event handlers.
/// </summary>
public class HandleLogoutRequest : IOpenIddictServerHandler<ProcessRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public HandleLogoutRequest([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseScopedHandler<HandleLogoutRequest>()
.SetOrder(ValidateLogoutRequest.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<HandleLogoutRequestContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.Append("Alternatively, enable the pass-through mode to handle them at a later stage.")
.ToString());
}
}
/// <summary>
/// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers.
/// </summary>
public class ApplyLogoutResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOpenIddictServerProvider _provider;
public ApplyLogoutResponse([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseScopedHandler<ApplyLogoutResponse<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
}
/// <summary>
/// Contains the logic responsible of rejecting logout requests that specify an invalid post_logout_redirect_uri parameter.
/// </summary>
public class ValidatePostLogoutRedirectUriParameter : IOpenIddictServerHandler<ValidateLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>()
.UseSingletonHandler<ValidatePostLogoutRedirectUriParameter>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ValidateClientPostLogoutRedirectUri : IOpenIddictServerHandler<ValidateLogoutRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequirePostLogoutRedirectUriParameter>()
.UseScopedHandler<ValidateClientPostLogoutRedirectUri>()
.SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async Task HandleAsync([NotNull] ValidateLogoutRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
async Task<bool> 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;
}
}
}
/// <summary>
/// Contains the logic responsible of inferring the redirect URL
/// used to send the response back to the client application.
/// </summary>
public class AttachPostLogoutRedirectUri : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.UseSingletonHandler<AttachPostLogoutRedirectUri>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of attaching the state to the response.
/// </summary>
public class AttachResponseState : IOpenIddictServerHandler<ApplyLogoutResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ApplyLogoutResponseContext>()
.UseSingletonHandler<AttachResponseState>()
.SetOrder(AttachPostLogoutRedirectUri.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
}
}
}

3
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);
/// <summary>
/// Contains the logic responsible of ensuring that the challenge response contains an appropriate error.

Loading…
Cancel
Save