Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
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.
 
 
 
 
 
 

503 lines
26 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.Extensions;
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.Logging;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
namespace OpenIddict.Server
{
public partial class OpenIddictServerProvider<TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TApplication : class where TAuthorization : class where TScope : class where TToken : class
{
public override async Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context)
{
var options = (OpenIddictServerOptions) context.Options;
// Reject requests using the unsupported request parameter.
if (!string.IsNullOrEmpty(context.Request.Request))
{
Logger.LogError("The authorization request was rejected because it contained " +
"an unsupported parameter: {Parameter}.", "request");
context.Reject(
error: OpenIdConnectConstants.Errors.RequestNotSupported,
description: "The 'request' parameter is not supported.");
return;
}
// Reject requests using the unsupported request_uri parameter.
if (!string.IsNullOrEmpty(context.Request.RequestUri))
{
Logger.LogError("The authorization request was rejected because it contained " +
"an unsupported parameter: {Parameter}.", "request_uri");
context.Reject(
error: OpenIdConnectConstants.Errors.RequestUriNotSupported,
description: "The 'request_uri' parameter is not supported.");
return;
}
// If a request_id parameter can be found in the authorization request,
// restore the complete authorization 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 authorization 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.AuthorizationRequest + context.Request.RequestId;
var payload = await options.Cache.GetAsync(key);
if (payload == null)
{
Logger.LogError("The authorization 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 authorization 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);
}
}
}
}
public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context)
{
var options = (OpenIddictServerOptions) context.Options;
// Note: the OpenID Connect server middleware supports authorization code, implicit, hybrid,
// none and custom flows but OpenIddict uses a stricter policy rejecting none and custum flows.
if (!context.Request.IsAuthorizationCodeFlow() && !context.Request.IsHybridFlow() && !context.Request.IsImplicitFlow())
{
Logger.LogError("The authorization request was rejected because the '{ResponseType}' " +
"response type is not supported.", context.Request.ResponseType);
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not supported.");
return;
}
// Reject code flow authorization requests if the authorization code flow is not enabled.
if (context.Request.IsAuthorizationCodeFlow() &&
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode))
{
Logger.LogError("The authorization request was rejected because " +
"the authorization code flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not allowed.");
return;
}
// Reject implicit flow authorization requests if the implicit flow is not enabled.
if (context.Request.IsImplicitFlow() && !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
{
Logger.LogError("The authorization request was rejected because the implicit flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not allowed.");
return;
}
// Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled.
if (context.Request.IsHybridFlow() && (!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
{
Logger.LogError("The authorization request was rejected because the " +
"authorization code flow or the implicit flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not allowed.");
return;
}
// Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'offline_access' scope is not allowed.");
return;
}
// Validates scopes, unless scope validation was explicitly disabled.
foreach (var scope in context.Request.GetScopes())
{
if (options.EnableScopeValidation && !options.Scopes.Contains(scope) &&
await Scopes.FindByNameAsync(scope) == null)
{
Logger.LogError("The authorization request was rejected because an " +
"unregistered scope was specified: {Scope}.", scope);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'scope' parameter is not valid.");
return;
}
}
// Note: the OpenID Connect server middleware supports the query, form_post and fragment response modes
// and doesn't reject unknown/custom modes until the ApplyAuthorizationResponse event is invoked.
// To ensure authorization requests are rejected early enough, an additional check is made by OpenIddict.
if (!string.IsNullOrEmpty(context.Request.ResponseMode) && !context.Request.IsFormPostResponseMode() &&
!context.Request.IsFragmentResponseMode() &&
!context.Request.IsQueryResponseMode())
{
Logger.LogError("The authorization request was rejected because the '{ResponseMode}' " +
"response mode is not supported.", context.Request.ResponseMode);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'response_mode' parameter is not supported.");
return;
}
// Note: redirect_uri is not required for pure OAuth2 requests
// but this provider uses a stricter policy making it mandatory,
// as required by the OpenID Connect core specification.
// See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.
if (string.IsNullOrEmpty(context.RedirectUri))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The mandatory 'redirect_uri' parameter is missing.");
return;
}
// Note: the OpenID Connect server middleware always ensures a
// code_challenge_method can't be specified without code_challenge.
if (!string.IsNullOrEmpty(context.Request.CodeChallenge))
{
// Since the default challenge method (plain) is explicitly disallowed,
// reject the authorization request if the code_challenge_method is missing.
if (string.IsNullOrEmpty(context.Request.CodeChallengeMethod))
{
Logger.LogError("The authorization request was rejected because the " +
"required 'code_challenge_method' parameter was missing.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'code_challenge_method' parameter must be specified.");
return;
}
// Disallow the use of the unsecure code_challenge_method=plain method.
// See https://tools.ietf.org/html/rfc7636#section-7.2 for more information.
if (string.Equals(context.Request.CodeChallengeMethod, OpenIdConnectConstants.CodeChallengeMethods.Plain))
{
Logger.LogError("The authorization request was rejected because the " +
"'code_challenge_method' parameter was set to 'plain'.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'code_challenge_method' parameter is not allowed.");
return;
}
// Reject authorization requests that contain response_type=token when a code_challenge is specified.
if (context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))
{
Logger.LogError("The authorization request was rejected because the " +
"specified response type was not compatible with PKCE.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'response_type' parameter is not allowed when using PKCE.");
return;
}
}
// Retrieve the application details corresponding to the requested client_id.
var application = await Applications.FindByClientIdAsync(context.ClientId);
if (application == null)
{
Logger.LogError("The authorization request was rejected because the client " +
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'client_id' parameter is invalid.");
return;
}
// Store the application entity as a request property to make it accessible
// from the other provider methods without having to call the store twice.
context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application);
// To prevent downgrade attacks, ensure that authorization requests returning an access token directly
// from the authorization endpoint are rejected if the client_id corresponds to a confidential application.
// Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting
// the token request if the client_id corresponds to an unauthenticated confidential client.
if (await Applications.IsConfidentialAsync(application) &&
context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "The specified 'response_type' parameter is not valid for this client application.");
return;
}
// Reject the request if the application is not allowed to use the authorization endpoint.
if (!await Applications.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Authorization))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the authorization endpoint.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "This client application is not allowed to use the authorization endpoint.");
return;
}
// Reject the request if the application is not allowed to use the authorization code flow.
if (context.Request.IsAuthorizationCodeFlow() && !await Applications.HasPermissionAsync(
application, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the authorization code flow.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "The client application is not allowed to use the authorization code flow.");
return;
}
// Reject the request if the application is not allowed to use the implicit flow.
if (context.Request.IsImplicitFlow() && !await Applications.HasPermissionAsync(
application, OpenIddictConstants.Permissions.GrantTypes.Implicit))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the implicit flow.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "The client application is not allowed to use the implicit flow.");
return;
}
// Reject the request if the application is not allowed to use the authorization code/implicit flows.
if (context.Request.IsHybridFlow() &&
(!await Applications.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode) ||
!await Applications.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.Implicit)))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the hybrid flow.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "The client application is not allowed to use the hybrid flow.");
return;
}
// Reject the request if the offline_access scope was request and if the
// application is not allowed to use the authorization code/implicit flows.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) &&
!await Applications.HasPermissionAsync(application, OpenIddictConstants.Permissions.GrantTypes.RefreshToken))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to request the 'offline_access' scope.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The client application is not allowed to use the 'offline_access' scope.");
return;
}
// Ensure that the specified redirect_uri is valid and is associated with the client application.
if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri))
{
Logger.LogError("The authorization request was rejected because the redirect_uri " +
"was invalid: '{RedirectUri}'.", context.RedirectUri);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The specified 'redirect_uri' parameter is not valid for this client application.");
return;
}
foreach (var scope in context.Request.GetScopes())
{
// Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes.
if (string.Equals(scope, OpenIdConnectConstants.Scopes.OfflineAccess, StringComparison.Ordinal) ||
string.Equals(scope, OpenIdConnectConstants.Scopes.OpenId, StringComparison.Ordinal))
{
continue;
}
// Reject the request if the application is not allowed to use the iterated scope.
if (!await Applications.HasPermissionAsync(application, OpenIddictConstants.Permissions.Prefixes.Scope + scope))
{
Logger.LogError("The authorization request was rejected because the application '{ClientId}' " +
"was not allowed to use the scope {Scope}.", context.ClientId, scope);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "This client application is not allowed to use the specified scope.");
return;
}
}
context.Validate();
}
public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context)
{
var options = (OpenIddictServerOptions) 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 authentication/registration 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 authorization 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 requests.
var key = OpenIddictConstants.Environment.AuthorizationRequest + 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 authorization 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;
}
context.SkipHandler();
}
public override async Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context)
{
var options = (OpenIddictServerOptions) context.Options;
// Remove the authorization 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.AuthorizationRequest + context.Request.RequestId;
// Note: the ApplyAuthorizationResponse event is called for both successful
// and errored authorization responses but discrimination is not necessary here,
// as the authorization 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.RedirectUri))
{
// 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 to return 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();
}
}
}
}
}