Browse Source

Introduce OpenIddictServerOptions.ResponseTypes/ResponseModes to support registering custom response types/modes

pull/816/head
Kévin Chalet 7 years ago
committed by GitHub
parent
commit
4a1d15f107
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      samples/Mvc.Server/Startup.cs
  2. 19
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  3. 3
      src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs
  4. 30
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  5. 167
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  6. 46
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  7. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  8. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  9. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  10. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  11. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  12. 18
      src/OpenIddict.Server/OpenIddictServerOptions.cs

11
samples/Mvc.Server/Startup.cs

@ -1,6 +1,7 @@
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;
@ -135,6 +136,16 @@ namespace Mvc.Server
app.UseStatusCodePagesWithReExecute("/error");
// Note: ASP.NET Core is impacted by a bug that prevents the status code pages
// from working correctly with endpoint routing. For more information, visit
// https://github.com/aspnet/AspNetCore/issues/13715#issuecomment-528929683.
app.Use((context, next) =>
{
context.SetEndpoint(null);
return next();
});
app.UseRouting();
app.UseAuthentication();

19
src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs

@ -79,6 +79,25 @@ namespace OpenIddict.Abstractions
return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(request.AcrValues, Separators.Space));
}
/// <summary>
/// Extracts the response types from an <see cref="OpenIddictRequest"/>.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
public static ImmutableHashSet<string> GetResponseTypes([NotNull] this OpenIddictRequest request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (string.IsNullOrEmpty(request.ResponseType))
{
return ImmutableHashSet.Create<string>(StringComparer.Ordinal);
}
return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(request.ResponseType, Separators.Space));
}
/// <summary>
/// Extracts the scopes from an <see cref="OpenIddictRequest"/>.
/// </summary>

3
src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs

@ -231,8 +231,7 @@ namespace OpenIddict.Abstractions
/// Gets all the parameters associated with this instance.
/// </summary>
/// <returns>The parameters associated with this instance.</returns>
public ImmutableDictionary<string, OpenIddictParameter> GetParameters()
=> Parameters.ToImmutableDictionary(StringComparer.Ordinal);
public IReadOnlyDictionary<string, OpenIddictParameter> GetParameters() => Parameters;
/// <summary>
/// Determines whether the current message contains the specified parameter.

30
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -172,6 +172,36 @@ namespace OpenIddict.Server
options.Scopes.Add(Scopes.OfflineAccess);
}
if (options.GrantTypes.Contains(GrantTypes.AuthorizationCode))
{
options.ResponseTypes.Add(ResponseTypes.Code);
}
if (options.GrantTypes.Contains(GrantTypes.Implicit))
{
options.ResponseTypes.Add(ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
options.ResponseTypes.Add(ResponseTypes.Token);
}
if (options.GrantTypes.Contains(GrantTypes.AuthorizationCode) && options.GrantTypes.Contains(GrantTypes.Implicit))
{
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token);
}
if (options.ResponseTypes.Count != 0)
{
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
if (options.ResponseTypes.Contains(ResponseTypes.Code))
{
options.ResponseModes.Add(ResponseModes.Query);
}
}
foreach (var key in options.EncryptionCredentials
.Select(credentials => credentials.Key)
.Concat(options.SigningCredentials.Select(credentials => credentials.Key)))

167
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -6,6 +6,7 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
@ -44,6 +45,7 @@ namespace OpenIddict.Server
ValidateRedirectUriParameter.Descriptor,
ValidateResponseTypeParameter.Descriptor,
ValidateResponseModeParameter.Descriptor,
ValidateScopeParameter.Descriptor,
ValidateNonceParameter.Descriptor,
ValidatePromptParameter.Descriptor,
ValidateCodeChallengeParameters.Descriptor,
@ -363,6 +365,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The authorization response was not correctly applied. To apply authorization response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyAuthorizationResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}
@ -632,32 +640,10 @@ namespace OpenIddict.Server
return default;
}
// Reject requests containing the id_token response_type if no openid scope has been received.
if (context.Request.HasResponseType(ResponseTypes.IdToken) && !context.Request.HasScope(Scopes.OpenId))
{
context.Logger.LogError("The authorization request was rejected because the 'openid' scope was missing.");
context.Reject(
error: Errors.InvalidRequest,
description: "The mandatory 'openid' scope is missing.");
return default;
}
// Reject requests containing the code response_type if the token endpoint has been disabled.
if (context.Request.HasResponseType(ResponseTypes.Code) && context.Options.TokenEndpointUris.Count == 0)
{
context.Logger.LogError("The authorization request was rejected because the authorization code flow was disabled.");
context.Reject(
error: Errors.UnsupportedResponseType,
description: "The specified 'response_type' is not supported by this server.");
return default;
}
// Reject requests that specify an unsupported response_type.
if (!context.Request.IsAuthorizationCodeFlow() && !context.Request.IsHybridFlow() && !context.Request.IsImplicitFlow())
var types = context.Request.GetResponseTypes();
if (!context.Options.ResponseTypes.Any(type =>
types.SetEquals(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))))
{
context.Logger.LogError("The authorization request was rejected because the '{ResponseType}' " +
"response type is not supported.", context.Request.ResponseType);
@ -669,71 +655,110 @@ namespace OpenIddict.Server
return default;
}
// Reject code flow authorization requests if the authorization code flow is not enabled.
if (context.Request.IsAuthorizationCodeFlow() && !context.Options.GrantTypes.Contains(GrantTypes.AuthorizationCode))
{
context.Logger.LogError("The authorization request was rejected because " +
"the authorization code flow was not enabled.");
return default;
}
}
context.Reject(
error: Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not allowed.");
/// <summary>
/// Contains the logic responsible of rejecting authorization requests that specify an invalid response_mode parameter.
/// </summary>
public class ValidateResponseModeParameter : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidateResponseModeParameter>()
.SetOrder(ValidateResponseTypeParameter.Descriptor.Order + 1_000)
.Build();
return default;
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ValidateAuthorizationRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Reject implicit flow authorization requests if the implicit flow is not enabled.
if (context.Request.IsImplicitFlow() && !context.Options.GrantTypes.Contains(GrantTypes.Implicit))
// response_mode=query (explicit or not) and a response_type containing id_token
// or token are not considered as a safe combination and MUST be rejected.
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security.
if (context.Request.IsQueryResponseMode() && (context.Request.HasResponseType(ResponseTypes.IdToken) ||
context.Request.HasResponseType(ResponseTypes.Token)))
{
context.Logger.LogError("The authorization request was rejected because the implicit flow was not enabled.");
context.Logger.LogError("The authorization request was rejected because the 'response_type'/'response_mode' " +
"combination was invalid: {ResponseType} ; {ResponseMode}.",
context.Request.ResponseType, context.Request.ResponseMode);
context.Reject(
error: Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not allowed.");
error: Errors.InvalidRequest,
description: "The specified 'response_type'/'response_mode' combination is invalid.");
return default;
}
// Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled.
if (context.Request.IsHybridFlow() && (!context.Options.GrantTypes.Contains(GrantTypes.AuthorizationCode) ||
!context.Options.GrantTypes.Contains(GrantTypes.Implicit)))
// Reject requests that specify an unsupported response_mode or don't specify a different response_mode
// if the default response_mode inferred from the response_type was explicitly disabled in the options.
if (!ValidateResponseMode(context.Request, context.Options))
{
context.Logger.LogError("The authorization request was rejected because the " +
"authorization code flow or the implicit flow was not enabled.");
context.Logger.LogError("The authorization request was rejected because the '{ResponseMode}' " +
"response mode is not supported.", context.Request.ResponseMode);
context.Reject(
error: Errors.UnsupportedResponseType,
description: "The specified 'response_type' parameter is not allowed.");
error: Errors.InvalidRequest,
description: "The specified 'response_mode' parameter is not supported.");
return default;
}
// Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(Scopes.OfflineAccess) && !context.Options.GrantTypes.Contains(GrantTypes.RefreshToken))
return default;
static bool ValidateResponseMode(OpenIddictRequest request, OpenIddictServerOptions options)
{
context.Reject(
error: Errors.InvalidRequest,
description: "The 'offline_access' scope is not allowed.");
// Note: both the fragment and query response modes are used as default response modes
// when using the implicit/hybrid and code flows if no explicit value was set.
// To ensure requests are rejected if the default response mode was manually disabled,
// the fragment and query response modes are checked first using the appropriate extensions.
return default;
}
if (request.IsFragmentResponseMode())
{
return options.ResponseModes.Contains(ResponseModes.Fragment);
}
return default;
if (request.IsQueryResponseMode())
{
return options.ResponseModes.Contains(ResponseModes.Query);
}
if (string.IsNullOrEmpty(request.ResponseMode))
{
return true;
}
return options.ResponseModes.Contains(request.ResponseMode);
}
}
}
/// <summary>
/// Contains the logic responsible of rejecting authorization requests that specify an invalid response_mode parameter.
/// Contains the logic responsible of rejecting authorization requests that don't specify a valid scope parameter.
/// </summary>
public class ValidateResponseModeParameter : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
public class ValidateScopeParameter : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidateResponseModeParameter>()
.SetOrder(ValidateResponseTypeParameter.Descriptor.Order + 1_000)
.UseSingletonHandler<ValidateScopeParameter>()
.SetOrder(ValidateResponseModeParameter.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -750,34 +775,24 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
// response_mode=query (explicit or not) and a response_type containing id_token
// or token are not considered as a safe combination and MUST be rejected.
// See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security.
if (context.Request.IsQueryResponseMode() && (context.Request.HasResponseType(ResponseTypes.IdToken) ||
context.Request.HasResponseType(ResponseTypes.Token)))
// Reject authorization requests containing the id_token response_type if no openid scope has been received.
if (context.Request.HasResponseType(ResponseTypes.IdToken) && !context.Request.HasScope(Scopes.OpenId))
{
context.Logger.LogError("The authorization request was rejected because the 'response_type'/'response_mode' " +
"combination was invalid: {ResponseType} ; {ResponseMode}.",
context.Request.ResponseType, context.Request.ResponseMode);
context.Logger.LogError("The authorization request was rejected because the 'openid' scope was missing.");
context.Reject(
error: Errors.InvalidRequest,
description: "The specified 'response_type'/'response_mode' combination is invalid.");
description: "The mandatory 'openid' scope is missing.");
return default;
}
// Reject requests that specify an unsupported response_mode.
if (!string.IsNullOrEmpty(context.Request.ResponseMode) && !context.Request.IsFormPostResponseMode() &&
!context.Request.IsFragmentResponseMode() &&
!context.Request.IsQueryResponseMode())
// Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(Scopes.OfflineAccess) && !context.Options.GrantTypes.Contains(GrantTypes.RefreshToken))
{
context.Logger.LogError("The authorization request was rejected because the '{ResponseMode}' " +
"response mode is not supported.", context.Request.ResponseMode);
context.Reject(
error: Errors.InvalidRequest,
description: "The specified 'response_mode' parameter is not supported.");
description: "The 'offline_access' scope is not allowed.");
return default;
}
@ -797,7 +812,7 @@ namespace OpenIddict.Server
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidateNonceParameter>()
.SetOrder(ValidateResponseModeParameter.Descriptor.Order + 1_000)
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.Build();
/// <summary>

46
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -353,6 +353,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The configuration response was not correctly applied. To apply configuration response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyConfigurationResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}
@ -499,11 +505,7 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
// Only populate grant_type_supported if the authorization or token endpoints are enabled.
if (context.AuthorizationEndpoint != null || context.TokenEndpoint != null)
{
context.GrantTypes.UnionWith(context.Options.GrantTypes);
}
context.GrantTypes.UnionWith(context.Options.GrantTypes);
return default;
}
@ -537,13 +539,7 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
// Only populate response_modes_supported if the authorization endpoint is enabled.
if (context.AuthorizationEndpoint != null)
{
context.ResponseModes.Add(ResponseModes.FormPost);
context.ResponseModes.Add(ResponseModes.Fragment);
context.ResponseModes.Add(ResponseModes.Query);
}
context.ResponseModes.UnionWith(context.Options.ResponseModes);
return default;
}
@ -577,25 +573,7 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (context.GrantTypes.Contains(GrantTypes.AuthorizationCode))
{
context.ResponseTypes.Add(ResponseTypes.Code);
}
if (context.GrantTypes.Contains(GrantTypes.AuthorizationCode) &&
context.GrantTypes.Contains(GrantTypes.Implicit))
{
context.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken);
context.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
context.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token);
}
if (context.GrantTypes.Contains(GrantTypes.Implicit))
{
context.ResponseTypes.Add(ResponseTypes.IdToken);
context.ResponseTypes.Add(ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
context.ResponseTypes.Add(ResponseTypes.Token);
}
context.ResponseTypes.UnionWith(context.Options.ResponseTypes);
return default;
}
@ -1224,6 +1202,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The cryptography response was not correctly applied. To apply cryptography response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyCryptographyResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}

6
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -355,6 +355,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The token response was not correctly applied. To apply token response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyTokenResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}

6
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -369,6 +369,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The introspection response was not correctly applied. To apply introspection response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyIntrospectionResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}

6
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -317,6 +317,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The revocation response was not correctly applied. To apply revocation response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyRevocationResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}

6
src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

@ -341,6 +341,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The revocation response was not correctly applied. To apply revocation response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyRevocationResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}

6
src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs

@ -345,6 +345,12 @@ namespace OpenIddict.Server
context.SkipRequest();
return;
}
throw new InvalidOperationException(new StringBuilder()
.Append("The userinfo response was not correctly applied. To apply userinfo response, ")
.Append("create a class implementing 'IOpenIddictServerHandler<ApplyUserinfoResponseContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
}

18
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
@ -123,6 +124,7 @@ namespace OpenIddict.Server
/// This option MUST be enabled with extreme caution and custom handlers MUST be registered to
/// properly validate OpenID Connect requests.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public bool EnableDegradedMode { get; set; }
/// <summary>
@ -189,6 +191,22 @@ namespace OpenIddict.Server
/// </summary>
public ISet<string> GrantTypes { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect response types enabled for this application.
/// Response types are automatically inferred from the supported standard grant types,
/// but additional values can be added for advanced scenarios (e.g custom type support).
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public ISet<string> ResponseTypes { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect response modes enabled for this application.
/// Response modes are automatically inferred from the supported standard grant types,
/// but additional values can be added for advanced scenarios (e.g custom mode support).
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public ISet<string> ResponseModes { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets a boolean indicating whether endpoint permissions should be ignored.
/// Setting this property to <c>true</c> is NOT recommended, unless all

Loading…
Cancel
Save