You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
11 KiB
221 lines
11 KiB
/*
|
|
* 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.IO;
|
|
using System.Threading.Tasks;
|
|
using AspNet.Security.OpenIdConnect.Primitives;
|
|
using AspNet.Security.OpenIdConnect.Server;
|
|
using JetBrains.Annotations;
|
|
using Microsoft.AspNetCore.Diagnostics;
|
|
using Microsoft.AspNetCore.WebUtilities;
|
|
using Microsoft.Extensions.Caching.Distributed;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Bson;
|
|
using Newtonsoft.Json.Linq;
|
|
using OpenIddict.Core;
|
|
|
|
namespace OpenIddict
|
|
{
|
|
public partial class OpenIddictProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
|
|
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
|
|
{
|
|
public override async Task ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context)
|
|
{
|
|
var options = (OpenIddictOptions) context.Options;
|
|
|
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
|
|
|
|
// 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 an error if request caching support was not enabled.
|
|
if (!options.EnableRequestCaching)
|
|
{
|
|
logger.LogError("The logout request was rejected because " +
|
|
"request caching support was not enabled.");
|
|
|
|
context.Reject(
|
|
error: OpenIdConnectConstants.Errors.InvalidRequest,
|
|
description: "The 'request_id' parameter is not supported.");
|
|
|
|
return;
|
|
}
|
|
|
|
// Note: the cache key is always prefixed with a specific marker
|
|
// to avoid collisions with the other types of cached requests.
|
|
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
|
|
|
|
var payload = await options.Cache.GetAsync(key);
|
|
if (payload == null)
|
|
{
|
|
logger.LogError("The logout request was rejected because an unknown " +
|
|
"or invalid request_id parameter was specified.");
|
|
|
|
context.Reject(
|
|
error: OpenIdConnectConstants.Errors.InvalidRequest,
|
|
description: "The specified 'request_id' parameter is invalid.");
|
|
|
|
return;
|
|
}
|
|
|
|
// Restore the logout request parameters from the serialized payload.
|
|
using (var reader = new BsonReader(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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context)
|
|
{
|
|
var applications = context.HttpContext.RequestServices.GetRequiredService<OpenIddictApplicationManager<TApplication>>();
|
|
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<OpenIddictProvider<TApplication, TAuthorization, TScope, TToken>>>();
|
|
|
|
// If an optional post_logout_redirect_uri was provided, validate it.
|
|
if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri))
|
|
{
|
|
if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri uri) || !uri.IsWellFormedOriginalString())
|
|
{
|
|
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: OpenIdConnectConstants.Errors.InvalidRequest,
|
|
description: "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.");
|
|
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(uri.Fragment))
|
|
{
|
|
logger.LogError("The logout request was rejected because the 'post_logout_redirect_uri' contained " +
|
|
"a URL fragment: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
|
|
|
|
context.Reject(
|
|
error: OpenIdConnectConstants.Errors.InvalidRequest,
|
|
description: "The 'post_logout_redirect_uri' parameter must not include a fragment.");
|
|
|
|
return;
|
|
}
|
|
|
|
if (!await applications.ValidatePostLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted))
|
|
{
|
|
logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " +
|
|
"was unknown: {PostLogoutRedirectUri}.", context.PostLogoutRedirectUri);
|
|
|
|
context.Reject(
|
|
error: OpenIdConnectConstants.Errors.InvalidRequest,
|
|
description: "The specified 'post_logout_redirect_uri' parameter is not valid.");
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
context.Validate();
|
|
}
|
|
|
|
public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context)
|
|
{
|
|
var options = (OpenIddictOptions) context.Options;
|
|
|
|
// If no request_id parameter can be found in the current request, assume the OpenID Connect
|
|
// request was not serialized yet and store the entire payload in the distributed cache
|
|
// to make it easier to flow across requests and internal/external logout workflows.
|
|
if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId))
|
|
{
|
|
// Generate a 256-bit request identifier using a crypto-secure random number generator.
|
|
var bytes = new byte[256 / 8];
|
|
options.RandomNumberGenerator.GetBytes(bytes);
|
|
context.Request.RequestId = Base64UrlEncoder.Encode(bytes);
|
|
|
|
// Store the serialized logout request parameters in the distributed cache.
|
|
var stream = new MemoryStream();
|
|
using (var writer = new BsonWriter(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 requests.
|
|
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
|
|
|
|
await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30),
|
|
SlidingExpiration = TimeSpan.FromMinutes(10)
|
|
});
|
|
|
|
// Create a new logout request containing only the request_id parameter.
|
|
var address = QueryHelpers.AddQueryString(
|
|
uri: context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host +
|
|
context.HttpContext.Request.PathBase + context.HttpContext.Request.Path,
|
|
name: OpenIdConnectConstants.Parameters.RequestId, value: context.Request.RequestId);
|
|
|
|
context.HttpContext.Response.Redirect(address);
|
|
|
|
// Mark the response as handled
|
|
// to skip the rest of the pipeline.
|
|
context.HandleResponse();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context)
|
|
{
|
|
var options = (OpenIddictOptions) context.Options;
|
|
|
|
// Remove the logout request from the distributed cache.
|
|
if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId))
|
|
{
|
|
// Note: the cache key is always prefixed with a specific marker
|
|
// to avoid collisions with the other types of cached requests.
|
|
var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId;
|
|
|
|
// 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.
|
|
await options.Cache.RemoveAsync(key);
|
|
}
|
|
|
|
if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) &&
|
|
string.IsNullOrEmpty(context.PostLogoutRedirectUri))
|
|
{
|
|
// Determine if the status code pages middleware has been enabled for this request.
|
|
// If it was not registered or enabled, let the OpenID Connect server middleware render
|
|
// a default error page instead of delegating the rendering to the status code middleware.
|
|
var feature = context.HttpContext.Features.Get<IStatusCodePagesFeature>();
|
|
if (feature != null && feature.Enabled)
|
|
{
|
|
// Replace the default status code by a 400 response.
|
|
context.HttpContext.Response.StatusCode = 400;
|
|
|
|
// Mark the request as fully handled to prevent the OpenID Connect server middleware
|
|
// 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.HandleResponse();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|