diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs index 7ce1daff..fce4ab35 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs @@ -150,15 +150,6 @@ namespace OpenIddict.Abstractions set => SetParameter(OpenIddictConstants.Parameters.IdToken, value); } - /// - /// Gets or sets the "realm" parameter. - /// - public string Realm - { - get => (string) GetParameter(OpenIddictConstants.Parameters.Realm); - set => SetParameter(OpenIddictConstants.Parameters.Realm, value); - } - /// /// Gets or sets the "refresh_token" parameter. /// diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs index c15bbcec..40634442 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs @@ -146,6 +146,21 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerAspNetCoreBuilder EnableStatusCodePagesIntegration() => Configure(options => options.EnableStatusCodePagesIntegration = true); + /// + /// Sets the realm returned to the caller as part of the WWW-Authenticate header. + /// + /// The issuer address. + /// The . + public OpenIddictServerAspNetCoreBuilder SetRealm([NotNull] string realm) + { + if (string.IsNullOrEmpty(realm)) + { + throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); + } + + return Configure(options => options.Realm = realm); + } + /// /// Sets the caching policy used by the authorization endpoint. /// Note: the specified policy is only used when caching is explicitly enabled. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs index 5724b625..87844bb4 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs @@ -31,7 +31,6 @@ namespace OpenIddict.Server.AspNetCore public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; - public const string Realm = ".realm"; public const string Scope = ".scope"; } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index 9a46c8c4..b5b2eeb0 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -43,7 +43,7 @@ namespace OpenIddict.Server.AspNetCore : base(options, logger, encoder, clock) => _provider = provider; - public async Task HandleRequestAsync() + protected override async Task InitializeHandlerAsync() { // Note: the transaction may be already attached when replaying an ASP.NET Core request // (e.g when using the built-in status code pages middleware with the re-execute mode). @@ -62,6 +62,18 @@ namespace OpenIddict.Server.AspNetCore var context = new ProcessRequestContext(transaction); await _provider.DispatchAsync(context); + // Store the context in the transaction so that it can be retrieved from HandleRequestAsync(). + transaction.SetProperty(typeof(ProcessRequestContext).FullName, context); + } + + public async Task HandleRequestAsync() + { + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); + + var context = transaction.GetProperty(typeof(ProcessRequestContext).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); + if (context.IsRequestHandled) { return true; @@ -108,11 +120,8 @@ namespace OpenIddict.Server.AspNetCore protected override async Task HandleAuthenticateAsync() { - var transaction = Context.Features.Get()?.Transaction; - if (transaction == null) - { - throw new InvalidOperationException("An identity cannot be extracted from this request."); - } + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); // Note: in many cases, the authentication token was already validated by the time this action is called // (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it, @@ -152,11 +161,8 @@ namespace OpenIddict.Server.AspNetCore protected override async Task HandleChallengeAsync([CanBeNull] AuthenticationProperties properties) { - var transaction = Context.Features.Get()?.Transaction; - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties(); @@ -209,11 +215,8 @@ namespace OpenIddict.Server.AspNetCore throw new ArgumentNullException(nameof(user)); } - var transaction = Context.Features.Get()?.Transaction; - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties(); @@ -259,11 +262,8 @@ namespace OpenIddict.Server.AspNetCore public async Task SignOutAsync([CanBeNull] AuthenticationProperties properties) { - var transaction = Context.Features.Get()?.Transaction; - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); var context = new ProcessSignOutContext(transaction) { diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs index 678f4c64..717a3879 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs @@ -31,6 +31,7 @@ namespace OpenIddict.Server.AspNetCore */ AttachHttpResponseCode.Descriptor, AttachWwwAuthenticateHeader.Descriptor, + ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor); } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 8fba93d5..6ceaeee6 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.IO; +using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -19,6 +20,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -331,7 +333,6 @@ namespace OpenIddict.Server.AspNetCore context.Response.Error = properties.GetString(Properties.Error); context.Response.ErrorDescription = properties.GetString(Properties.ErrorDescription); context.Response.ErrorUri = properties.GetString(Properties.ErrorUri); - context.Response.Realm = properties.GetString(Properties.Realm); context.Response.Scope = properties.GetString(Properties.Scope); } @@ -901,11 +902,6 @@ namespace OpenIddict.Server.AspNetCore 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; @@ -992,6 +988,11 @@ namespace OpenIddict.Server.AspNetCore /// public class AttachWwwAuthenticateHeader : IOpenIddictServerHandler where TContext : BaseRequestContext { + private readonly IOptionsMonitor _options; + + public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor options) + => _options = options; + /// /// Gets the default descriptor definition assigned to this handler. /// @@ -999,7 +1000,7 @@ namespace OpenIddict.Server.AspNetCore = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .SetOrder(ProcessChallengeErrorResponse.Descriptor.Order - 1_000) .Build(); /// @@ -1016,11 +1017,6 @@ namespace OpenIddict.Server.AspNetCore 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; @@ -1053,98 +1049,107 @@ namespace OpenIddict.Server.AspNetCore return default; } - // Optimization: avoid allocating a StringBuilder if the - // WWW-Authenticate header doesn't contain any parameter. - if (string.IsNullOrEmpty(context.Response.Realm) && - string.IsNullOrEmpty(context.Response.Error) && - string.IsNullOrEmpty(context.Response.ErrorDescription) && - string.IsNullOrEmpty(context.Response.ErrorUri) && - string.IsNullOrEmpty(context.Response.Scope)) - { - response.Headers.Append(HeaderNames.WWWAuthenticate, scheme); + var parameters = new Dictionary(StringComparer.Ordinal); - return default; - } - - var builder = new StringBuilder(scheme); - - // Append the realm if one was specified. - if (!string.IsNullOrEmpty(context.Response.Realm)) + // If a realm was configured in the options, attach it to the parameters. + if (!string.IsNullOrEmpty(_options.CurrentValue.Realm)) { - builder.Append(' '); - builder.Append(Parameters.Realm); - builder.Append("=\""); - builder.Append(context.Response.Realm.Replace("\"", "\\\"")); - builder.Append('"'); + parameters[Parameters.Realm] = _options.CurrentValue.Realm; } - // Append the error if one was specified. - if (!string.IsNullOrEmpty(context.Response.Error)) + foreach (var parameter in context.Response.GetParameters()) { - if (!string.IsNullOrEmpty(context.Response.Realm)) + // Note: the error details are only included if the error was not caused by a missing token, as recommended + // by the OAuth 2.0 bearer specification: https://tools.ietf.org/html/rfc6750#section-3.1. + if (string.Equals(context.Response.Error, Errors.MissingToken, StringComparison.Ordinal) && + (string.Equals(parameter.Key, Parameters.Error, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorDescription, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorUri, StringComparison.Ordinal))) { - builder.Append(','); + continue; } - builder.Append(' '); - builder.Append(Parameters.Error); - builder.Append("=\""); - builder.Append(context.Response.Error.Replace("\"", "\\\"")); - builder.Append('"'); - } - - // Append the error_description if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error)) + // Ignore values that can't be represented as unique strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) { - builder.Append(','); + continue; } + parameters[parameter.Key] = value; + } + + var builder = new StringBuilder(scheme); + + foreach (var parameter in parameters) + { builder.Append(' '); - builder.Append(Parameters.ErrorDescription); - builder.Append("=\""); - builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\"")); + builder.Append(parameter.Key); + builder.Append('='); builder.Append('"'); + builder.Append(parameter.Value.Replace("\"", "\\\"")); + builder.Append('"'); + builder.Append(','); } - // Append the error_uri if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorUri)) + // If the WWW-Authenticate header ends with a comma, remove it. + if (builder[builder.Length - 1] == ',') { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - builder.Append(','); - } + builder.Remove(builder.Length - 1, 1); + } - builder.Append(' '); - builder.Append(Parameters.ErrorUri); - builder.Append("=\""); - builder.Append(context.Response.ErrorUri.Replace("\"", "\\\"")); - builder.Append('"'); + response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); + + return default; + } + } + + /// + /// Contains the logic responsible of processing challenge responses that contain a WWW-Authenticate header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessChallengeErrorResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - // Append the scope if one was specified. - if (!string.IsNullOrEmpty(context.Response.Scope)) + // 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) { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription) || - !string.IsNullOrEmpty(context.Response.ErrorUri)) - { - builder.Append(','); - } + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } - builder.Append(' '); - builder.Append(Parameters.Scope); - builder.Append("=\""); - builder.Append(context.Response.Scope.Replace("\"", "\\\"")); - builder.Append('"'); + // If the response doesn't contain a WWW-Authenticate header, don't return an empty response. + if (!response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)) + { + return default; } - response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); + context.Logger.LogInformation("The response was successfully returned as an empty challenge response."); + context.HandleRequest(); return default; } @@ -1180,11 +1185,6 @@ namespace OpenIddict.Server.AspNetCore 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; @@ -1298,11 +1298,6 @@ namespace OpenIddict.Server.AspNetCore 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; diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs index fd311c26..3ad63dad 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs @@ -93,6 +93,12 @@ namespace OpenIddict.Server.AspNetCore /// public bool EnableStatusCodePagesIntegration { get; set; } + /// + /// Gets or sets the optional "realm" value returned to + /// the caller as part of the WWW-Authenticate header. + /// + public string Realm { get; set; } + /// /// Gets or sets the caching policy used by the authorization endpoint. /// diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs index 18790059..3a6a6a6e 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs @@ -138,6 +138,21 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerOwinBuilder EnableLogoutEndpointCaching() => Configure(options => options.EnableLogoutEndpointCaching = true); + /// + /// Sets the realm returned to the caller as part of the WWW-Authenticate header. + /// + /// The issuer address. + /// The . + public OpenIddictServerOwinBuilder SetRealm([NotNull] string realm) + { + if (string.IsNullOrEmpty(realm)) + { + throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); + } + + return Configure(options => options.Realm = realm); + } + /// /// Sets the caching policy used by the authorization endpoint. /// Note: the specified policy is only used when caching is explicitly enabled. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs index b3a4956e..d73c5d09 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs @@ -31,7 +31,6 @@ namespace OpenIddict.Server.Owin public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; - public const string Realm = ".realm"; public const string Scope = ".scope"; } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs index a0825908..d00c5159 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -33,7 +33,7 @@ namespace OpenIddict.Server.Owin public OpenIddictServerOwinHandler([NotNull] IOpenIddictServerProvider provider) => _provider = provider; - public override async Task InvokeAsync() + protected override async Task InitializeCoreAsync() { // Note: the transaction may be already attached when replaying an OWIN request // (e.g when using a status code pages middleware re-invoking the OWIN pipeline). @@ -52,6 +52,18 @@ namespace OpenIddict.Server.Owin var context = new ProcessRequestContext(transaction); await _provider.DispatchAsync(context); + // Store the context in the transaction so that it can be retrieved from InvokeAsync(). + transaction.SetProperty(typeof(ProcessRequestContext).FullName, context); + } + + public override async Task InvokeAsync() + { + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); + + var context = transaction.GetProperty(typeof(ProcessRequestContext).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); + if (context.IsRequestHandled) { return true; @@ -101,7 +113,7 @@ namespace OpenIddict.Server.Owin var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); if (transaction == null) { - throw new InvalidOperationException("An identity cannot be extracted from this request."); + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); } // Note: in many cases, the authentication token was already validated by the time this action is called @@ -152,14 +164,14 @@ namespace OpenIddict.Server.Owin // OpenIddictServerOwinMiddleware is assumed to be the only middleware allowed to write // to the response stream when a response grant (sign-in/out or challenge) was applied. + // Note: unlike the ASP.NET Core host, the OWIN host MUST check whether the status code + // corresponds to a challenge response, as LookupChallenge() will always return a non-null + // value when active authentication is used, even if no challenge was actually triggered. var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); - if (challenge != null) + if (challenge != null && (Response.StatusCode == 401 || Response.StatusCode == 403)) { - var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = challenge.Properties ?? new AuthenticationProperties(); @@ -205,11 +217,8 @@ namespace OpenIddict.Server.Owin var signin = Helper.LookupSignIn(Options.AuthenticationType); if (signin != null) { - var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = signin.Properties ?? new AuthenticationProperties(); @@ -256,11 +265,8 @@ namespace OpenIddict.Server.Owin var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); if (signout != null) { - var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = signout.Properties ?? new AuthenticationProperties(); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs index ba81694a..b4a7db78 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs @@ -31,6 +31,7 @@ namespace OpenIddict.Server.Owin */ AttachHttpResponseCode.Descriptor, AttachWwwAuthenticateHeader.Descriptor, + ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor); } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 36d6ad68..0c9cf050 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -15,6 +15,7 @@ using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Owin; using Microsoft.Owin.Security; using OpenIddict.Abstractions; @@ -320,7 +321,6 @@ namespace OpenIddict.Server.Owin context.Response.Error = GetProperty(properties, Properties.Error); context.Response.ErrorDescription = GetProperty(properties, Properties.ErrorDescription); context.Response.ErrorUri = GetProperty(properties, Properties.ErrorUri); - context.Response.Realm = GetProperty(properties, Properties.Realm); context.Response.Scope = GetProperty(properties, Properties.Scope); } @@ -833,11 +833,6 @@ namespace OpenIddict.Server.Owin 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 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; @@ -924,6 +919,11 @@ namespace OpenIddict.Server.Owin /// public class AttachWwwAuthenticateHeader : IOpenIddictServerHandler where TContext : BaseRequestContext { + private readonly IOptionsMonitor _options; + + public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor options) + => _options = options; + /// /// Gets the default descriptor definition assigned to this handler. /// @@ -931,7 +931,7 @@ namespace OpenIddict.Server.Owin = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .SetOrder(ProcessChallengeErrorResponse.Descriptor.Order - 1_000) .Build(); /// @@ -948,11 +948,6 @@ namespace OpenIddict.Server.Owin 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 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; @@ -985,98 +980,107 @@ namespace OpenIddict.Server.Owin return default; } - // Optimization: avoid allocating a StringBuilder if the - // WWW-Authenticate header doesn't contain any parameter. - if (string.IsNullOrEmpty(context.Response.Realm) && - string.IsNullOrEmpty(context.Response.Error) && - string.IsNullOrEmpty(context.Response.ErrorDescription) && - string.IsNullOrEmpty(context.Response.ErrorUri) && - string.IsNullOrEmpty(context.Response.Scope)) - { - response.Headers.Append("WWW-Authenticate", scheme); - - return default; - } - - var builder = new StringBuilder(scheme); + var parameters = new Dictionary(StringComparer.Ordinal); - // Append the realm if one was specified. - if (!string.IsNullOrEmpty(context.Response.Realm)) + // If a realm was configured in the options, attach it to the parameters. + if (!string.IsNullOrEmpty(_options.CurrentValue.Realm)) { - builder.Append(' '); - builder.Append(Parameters.Realm); - builder.Append("=\""); - builder.Append(context.Response.Realm.Replace("\"", "\\\"")); - builder.Append('"'); + parameters[Parameters.Realm] = _options.CurrentValue.Realm; } - // Append the error if one was specified. - if (!string.IsNullOrEmpty(context.Response.Error)) + foreach (var parameter in context.Response.GetParameters()) { - if (!string.IsNullOrEmpty(context.Response.Realm)) + // Note: the error details are only included if the error was not caused by a missing token, as recommended + // by the OAuth 2.0 bearer specification: https://tools.ietf.org/html/rfc6750#section-3.1. + if (string.Equals(context.Response.Error, Errors.MissingToken, StringComparison.Ordinal) && + (string.Equals(parameter.Key, Parameters.Error, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorDescription, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorUri, StringComparison.Ordinal))) { - builder.Append(','); + continue; } - builder.Append(' '); - builder.Append(Parameters.Error); - builder.Append("=\""); - builder.Append(context.Response.Error.Replace("\"", "\\\"")); - builder.Append('"'); - } - - // Append the error_description if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error)) + // Ignore values that can't be represented as unique strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) { - builder.Append(','); + continue; } + parameters[parameter.Key] = value; + } + + var builder = new StringBuilder(scheme); + + foreach (var parameter in parameters) + { builder.Append(' '); - builder.Append(Parameters.ErrorDescription); - builder.Append("=\""); - builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\"")); + builder.Append(parameter.Key); + builder.Append('='); + builder.Append('"'); + builder.Append(parameter.Value.Replace("\"", "\\\"")); builder.Append('"'); + builder.Append(','); } - // Append the error_uri if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorUri)) + // If the WWW-Authenticate header ends with a comma, remove it. + if (builder[builder.Length - 1] == ',') { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - builder.Append(','); - } + builder.Remove(builder.Length - 1, 1); + } - builder.Append(' '); - builder.Append(Parameters.ErrorUri); - builder.Append("=\""); - builder.Append(context.Response.ErrorUri.Replace("\"", "\\\"")); - builder.Append('"'); + response.Headers.Append("WWW-Authenticate", builder.ToString()); + + return default; + } + } + + /// + /// Contains the logic responsible of processing challenge responses that contain a WWW-Authenticate header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessChallengeErrorResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - // Append the scope if one was specified. - if (!string.IsNullOrEmpty(context.Response.Scope)) + // 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) { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription) || - !string.IsNullOrEmpty(context.Response.ErrorUri)) - { - builder.Append(','); - } + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } - builder.Append(' '); - builder.Append(Parameters.Scope); - builder.Append("=\""); - builder.Append(context.Response.Scope.Replace("\"", "\\\"")); - builder.Append('"'); + // If the response doesn't contain a WWW-Authenticate header, don't return an empty response. + if (!response.Headers.ContainsKey("WWW-Authenticate")) + { + return default; } - response.Headers.Append("WWW-Authenticate", builder.ToString()); + context.Logger.LogInformation("The response was successfully returned as an empty challenge response."); + context.HandleRequest(); return default; } @@ -1112,11 +1116,6 @@ namespace OpenIddict.Server.Owin 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 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; diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs index bfb8de6c..90711e45 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs @@ -93,6 +93,12 @@ namespace OpenIddict.Server.Owin /// public bool EnableLogoutEndpointCaching { get; set; } + /// + /// Gets or sets the optional "realm" value returned to + /// the caller as part of the WWW-Authenticate header. + /// + public string Realm { get; set; } + /// /// Gets or sets the caching policy used by the authorization endpoint. /// diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 29981754..b16031eb 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -1777,21 +1777,6 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Issuer = address); } - /// - /// Sets the realm returned to the caller as part of challenge responses. - /// - /// The issuer address. - /// The . - public OpenIddictServerBuilder SetRealm([NotNull] string realm) - { - if (string.IsNullOrEmpty(realm)) - { - throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); - } - - return Configure(options => options.Realm = realm); - } - /// /// Configures OpenIddict to use reference tokens, so that the token and code payloads /// are stored in the database (only an identifier is returned to the client application). diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index b3411be5..db79d9c9 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -1140,15 +1140,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If error details were explicitly set by the application, don't override them. - if (!string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription) || - !string.IsNullOrEmpty(context.Response.ErrorUri)) - { - return default; - } - - context.Response.Error = context.EndpointType switch + context.Response.Error ??= context.EndpointType switch { OpenIddictServerEndpointType.Authorization => Errors.AccessDenied, OpenIddictServerEndpointType.Token => Errors.InvalidGrant, @@ -1158,7 +1150,7 @@ namespace OpenIddict.Server _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") }; - context.Response.ErrorDescription = context.EndpointType switch + context.Response.ErrorDescription ??= context.EndpointType switch { OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.", OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.", @@ -1168,8 +1160,6 @@ namespace OpenIddict.Server _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") }; - context.Response.Realm = context.Options.Realm; - return default; } } diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 7c4aea4f..96962a34 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -330,12 +330,6 @@ namespace OpenIddict.Server /// public bool IgnoreScopePermissions { get; set; } - /// - /// Gets or sets the optional "realm" value returned to - /// the caller as part of challenge responses. - /// - public string Realm { get; set; } - /// /// Gets the OAuth 2.0/OpenID Connect scopes enabled for this application. /// diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs index 0bda0420..15392b03 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs @@ -48,6 +48,21 @@ namespace Microsoft.Extensions.DependencyInjection return this; } + /// + /// Sets the realm returned to the caller as part of the WWW-Authenticate header. + /// + /// The issuer address. + /// The . + public OpenIddictValidationAspNetCoreBuilder SetRealm([NotNull] string realm) + { + if (string.IsNullOrEmpty(realm)) + { + throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); + } + + return Configure(options => options.Realm = realm); + } + /// /// Determines whether the specified object is equal to the current object. /// diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs index cf32b687..2cbd63ce 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs @@ -22,7 +22,6 @@ namespace OpenIddict.Validation.AspNetCore public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; - public const string Realm = ".realm"; public const string Scope = ".scope"; } } diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs index 3d49eac8..03138a37 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs @@ -40,7 +40,7 @@ namespace OpenIddict.Validation.AspNetCore : base(options, logger, encoder, clock) => _provider = provider; - public async Task HandleRequestAsync() + protected override async Task InitializeHandlerAsync() { // Note: the transaction may be already attached when replaying an ASP.NET Core request // (e.g when using the built-in status code pages middleware with the re-execute mode). @@ -59,6 +59,18 @@ namespace OpenIddict.Validation.AspNetCore var context = new ProcessRequestContext(transaction); await _provider.DispatchAsync(context); + // Store the context in the transaction so that it can be retrieved from HandleRequestAsync(). + transaction.SetProperty(typeof(ProcessRequestContext).FullName, context); + } + + public async Task HandleRequestAsync() + { + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); + + var context = transaction.GetProperty(typeof(ProcessRequestContext).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); + if (context.IsRequestHandled) { return true; @@ -105,11 +117,8 @@ namespace OpenIddict.Validation.AspNetCore protected override async Task HandleAuthenticateAsync() { - var transaction = Context.Features.Get()?.Transaction; - if (transaction == null) - { - throw new InvalidOperationException("An identity cannot be extracted from this request."); - } + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); // Note: in many cases, the authentication token was already validated by the time this action is called // (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it, @@ -149,11 +158,8 @@ namespace OpenIddict.Validation.AspNetCore protected override async Task HandleChallengeAsync([CanBeNull] AuthenticationProperties properties) { - var transaction = Context.Features.Get()?.Transaction; - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Features.Get()?.Transaction ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties(); diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index 9aac6042..daf0fc32 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs @@ -5,9 +5,11 @@ */ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.IO; +using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -17,6 +19,7 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -48,11 +51,13 @@ namespace OpenIddict.Validation.AspNetCore AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, + ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor, AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, + ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor); /// @@ -268,7 +273,6 @@ namespace OpenIddict.Validation.AspNetCore context.Response.Error = properties.GetString(Properties.Error); context.Response.ErrorDescription = properties.GetString(Properties.ErrorDescription); context.Response.ErrorUri = properties.GetString(Properties.ErrorUri); - context.Response.Realm = properties.GetString(Properties.Realm); context.Response.Scope = properties.GetString(Properties.Scope); } @@ -306,11 +310,6 @@ namespace OpenIddict.Validation.AspNetCore 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; @@ -389,6 +388,11 @@ namespace OpenIddict.Validation.AspNetCore /// public class AttachWwwAuthenticateHeader : IOpenIddictValidationHandler where TContext : BaseRequestContext { + private readonly IOptionsMonitor _options; + + public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor options) + => _options = options; + /// /// Gets the default descriptor definition assigned to this handler. /// @@ -396,7 +400,7 @@ namespace OpenIddict.Validation.AspNetCore = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .SetOrder(ProcessChallengeErrorResponse.Descriptor.Order - 1_000) .Build(); /// @@ -413,11 +417,6 @@ namespace OpenIddict.Validation.AspNetCore 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; @@ -441,98 +440,107 @@ namespace OpenIddict.Validation.AspNetCore return default; } - // Optimization: avoid allocating a StringBuilder if the - // WWW-Authenticate header doesn't contain any parameter. - if (string.IsNullOrEmpty(context.Response.Realm) && - string.IsNullOrEmpty(context.Response.Error) && - string.IsNullOrEmpty(context.Response.ErrorDescription) && - string.IsNullOrEmpty(context.Response.ErrorUri) && - string.IsNullOrEmpty(context.Response.Scope)) - { - response.Headers.Append(HeaderNames.WWWAuthenticate, scheme); + var parameters = new Dictionary(StringComparer.Ordinal); - return default; - } - - var builder = new StringBuilder(scheme); - - // Append the realm if one was specified. - if (!string.IsNullOrEmpty(context.Response.Realm)) + // If a realm was configured in the options, attach it to the parameters. + if (!string.IsNullOrEmpty(_options.CurrentValue.Realm)) { - builder.Append(' '); - builder.Append(Parameters.Realm); - builder.Append("=\""); - builder.Append(context.Response.Realm.Replace("\"", "\\\"")); - builder.Append('"'); + parameters[Parameters.Realm] = _options.CurrentValue.Realm; } - // Append the error if one was specified. - if (!string.IsNullOrEmpty(context.Response.Error)) + foreach (var parameter in context.Response.GetParameters()) { - if (!string.IsNullOrEmpty(context.Response.Realm)) + // Note: the error details are only included if the error was not caused by a missing token, as recommended + // by the OAuth 2.0 bearer specification: https://tools.ietf.org/html/rfc6750#section-3.1. + if (string.Equals(context.Response.Error, Errors.MissingToken, StringComparison.Ordinal) && + (string.Equals(parameter.Key, Parameters.Error, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorDescription, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorUri, StringComparison.Ordinal))) { - builder.Append(','); + continue; } - builder.Append(' '); - builder.Append(Parameters.Error); - builder.Append("=\""); - builder.Append(context.Response.Error.Replace("\"", "\\\"")); - builder.Append('"'); - } - - // Append the error_description if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error)) + // Ignore values that can't be represented as unique strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) { - builder.Append(','); + continue; } + parameters[parameter.Key] = value; + } + + var builder = new StringBuilder(scheme); + + foreach (var parameter in parameters) + { builder.Append(' '); - builder.Append(Parameters.ErrorDescription); - builder.Append("=\""); - builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\"")); + builder.Append(parameter.Key); + builder.Append('='); + builder.Append('"'); + builder.Append(parameter.Value.Replace("\"", "\\\"")); builder.Append('"'); + builder.Append(','); } - // Append the error_uri if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorUri)) + // If the WWW-Authenticate header ends with a comma, remove it. + if (builder[builder.Length - 1] == ',') { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - builder.Append(','); - } + builder.Remove(builder.Length - 1, 1); + } - builder.Append(' '); - builder.Append(Parameters.ErrorUri); - builder.Append("=\""); - builder.Append(context.Response.ErrorUri.Replace("\"", "\\\"")); - builder.Append('"'); + response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); + + return default; + } + } + + /// + /// Contains the logic responsible of processing challenge responses that contain a WWW-Authenticate header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ProcessChallengeErrorResponse : IOpenIddictValidationHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - // Append the scope if one was specified. - if (!string.IsNullOrEmpty(context.Response.Scope)) + // 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) { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription) || - !string.IsNullOrEmpty(context.Response.ErrorUri)) - { - builder.Append(','); - } + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } - builder.Append(' '); - builder.Append(Parameters.Scope); - builder.Append("=\""); - builder.Append(context.Response.Scope.Replace("\"", "\\\"")); - builder.Append('"'); + // If the response doesn't contain a WWW-Authenticate header, don't return an empty response. + if (!response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)) + { + return default; } - response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString()); + context.Logger.LogInformation("The response was successfully returned as an empty challenge response."); + context.HandleRequest(); return default; } @@ -568,11 +576,6 @@ namespace OpenIddict.Validation.AspNetCore 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; diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs index 28c76ebb..37577ef1 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs @@ -13,5 +13,10 @@ namespace OpenIddict.Validation.AspNetCore /// public class OpenIddictValidationAspNetCoreOptions : AuthenticationSchemeOptions { + /// + /// Gets or sets the optional "realm" value returned to + /// the caller as part of the WWW-Authenticate header. + /// + public string Realm { get; set; } } } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs index 0c101b1e..1a78afe0 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel; using JetBrains.Annotations; +using Microsoft.Owin.Security; using OpenIddict.Validation.Owin; namespace Microsoft.Extensions.DependencyInjection @@ -48,6 +49,36 @@ namespace Microsoft.Extensions.DependencyInjection return this; } + /// + /// Configures the OpenIddict validation OWIN integration to use active authentication. + /// When using active authentication, the principal resolved from the access token is + /// attached to the request context and 401/403 responses are automatically handled without + /// requiring an explicit call to . + /// + /// + /// Using active authentication is strongly discouraged in applications using a cookie + /// authentication middleware configured to use active authentication, as both middleware + /// will be invoked when handling 401 responses, which will result in invalid responses. + /// + /// The . + public OpenIddictValidationOwinBuilder UseActiveAuthentication() + => Configure(options => options.AuthenticationMode = AuthenticationMode.Active); + + /// + /// Sets the realm returned to the caller as part of the WWW-Authenticate header. + /// + /// The issuer address. + /// The . + public OpenIddictValidationOwinBuilder SetRealm([NotNull] string realm) + { + if (string.IsNullOrEmpty(realm)) + { + throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); + } + + return Configure(options => options.Realm = realm); + } + /// /// Determines whether the specified object is equal to the current object. /// diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs index 7150cd77..1ab8d75f 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs @@ -22,7 +22,6 @@ namespace OpenIddict.Validation.Owin public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; - public const string Realm = ".realm"; public const string Scope = ".scope"; } } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs index 25431a4c..d7584ec8 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs @@ -33,7 +33,7 @@ namespace OpenIddict.Validation.Owin public OpenIddictValidationOwinHandler([NotNull] IOpenIddictValidationProvider provider) => _provider = provider; - public override async Task InvokeAsync() + protected override async Task InitializeCoreAsync() { // Note: the transaction may be already attached when replaying an OWIN request // (e.g when using a status code pages middleware re-invoking the OWIN pipeline). @@ -52,6 +52,18 @@ namespace OpenIddict.Validation.Owin var context = new ProcessRequestContext(transaction); await _provider.DispatchAsync(context); + // Store the context in the transaction so that it can be retrieved from InvokeAsync(). + transaction.SetProperty(typeof(ProcessRequestContext).FullName, context); + } + + public override async Task InvokeAsync() + { + var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); + + var context = transaction.GetProperty(typeof(ProcessRequestContext).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); + if (context.IsRequestHandled) { return true; @@ -98,11 +110,8 @@ namespace OpenIddict.Validation.Owin protected override async Task AuthenticateCoreAsync() { - var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName); - if (transaction?.Request == null) - { - throw new InvalidOperationException("An identity cannot be extracted from this request."); - } + var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); // Note: in many cases, the authentication token was already validated by the time this action is called // (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it, @@ -152,14 +161,14 @@ namespace OpenIddict.Validation.Owin // OpenIddictValidationOwinMiddleware is assumed to be the only middleware allowed to write // to the response stream when a response grant (sign-in/out or challenge) was applied. + // Note: unlike the ASP.NET Core host, the OWIN host MUST check whether the status code + // corresponds to a challenge response, as LookupChallenge() will always return a non-null + // value when active authentication is used, even if no challenge was actually triggered. var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); - if (challenge != null) + if (challenge != null && (Response.StatusCode == 401 || Response.StatusCode == 403)) { - var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName); - if (transaction == null) - { - throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); - } + var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName) ?? + throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context."); transaction.Properties[typeof(AuthenticationProperties).FullName] = challenge.Properties ?? new AuthenticationProperties(); diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index ece36bfb..ae172cf6 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.IO; @@ -14,6 +15,7 @@ using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Owin.Security; using OpenIddict.Abstractions; using Owin; @@ -46,11 +48,13 @@ namespace OpenIddict.Validation.Owin AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, + ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor, AttachHttpResponseCode.Descriptor, AttachCacheControlHeader.Descriptor, AttachWwwAuthenticateHeader.Descriptor, + ProcessChallengeErrorResponse.Descriptor, ProcessJsonResponse.Descriptor); /// @@ -267,7 +271,6 @@ namespace OpenIddict.Validation.Owin context.Response.Error = GetProperty(properties, Properties.Error); context.Response.ErrorDescription = GetProperty(properties, Properties.ErrorDescription); context.Response.ErrorUri = GetProperty(properties, Properties.ErrorUri); - context.Response.Realm = GetProperty(properties, Properties.Realm); context.Response.Scope = GetProperty(properties, Properties.Scope); } @@ -308,11 +311,6 @@ namespace OpenIddict.Validation.Owin 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 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; @@ -391,6 +389,11 @@ namespace OpenIddict.Validation.Owin /// public class AttachWwwAuthenticateHeader : IOpenIddictValidationHandler where TContext : BaseRequestContext { + private readonly IOptionsMonitor _options; + + public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor options) + => _options = options; + /// /// Gets the default descriptor definition assigned to this handler. /// @@ -398,7 +401,7 @@ namespace OpenIddict.Validation.Owin = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .SetOrder(ProcessChallengeErrorResponse.Descriptor.Order - 1_000) .Build(); /// @@ -415,11 +418,6 @@ namespace OpenIddict.Validation.Owin 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 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; @@ -448,98 +446,107 @@ namespace OpenIddict.Validation.Owin return default; } - // Optimization: avoid allocating a StringBuilder if the - // WWW-Authenticate header doesn't contain any parameter. - if (string.IsNullOrEmpty(context.Response.Realm) && - string.IsNullOrEmpty(context.Response.Error) && - string.IsNullOrEmpty(context.Response.ErrorDescription) && - string.IsNullOrEmpty(context.Response.ErrorUri) && - string.IsNullOrEmpty(context.Response.Scope)) - { - response.Headers.Append("WWW-Authenticate", scheme); + var parameters = new Dictionary(StringComparer.Ordinal); - return default; - } - - var builder = new StringBuilder(scheme); - - // Append the realm if one was specified. - if (!string.IsNullOrEmpty(context.Response.Realm)) + // If a realm was configured in the options, attach it to the parameters. + if (!string.IsNullOrEmpty(_options.CurrentValue.Realm)) { - builder.Append(' '); - builder.Append(Parameters.Realm); - builder.Append("=\""); - builder.Append(context.Response.Realm.Replace("\"", "\\\"")); - builder.Append('"'); + parameters[Parameters.Realm] = _options.CurrentValue.Realm; } - // Append the error if one was specified. - if (!string.IsNullOrEmpty(context.Response.Error)) + foreach (var parameter in context.Response.GetParameters()) { - if (!string.IsNullOrEmpty(context.Response.Realm)) + // Note: the error details are only included if the error was not caused by a missing token, as recommended + // by the OAuth 2.0 bearer specification: https://tools.ietf.org/html/rfc6750#section-3.1. + if (string.Equals(context.Response.Error, Errors.MissingToken, StringComparison.Ordinal) && + (string.Equals(parameter.Key, Parameters.Error, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorDescription, StringComparison.Ordinal) || + string.Equals(parameter.Key, Parameters.ErrorUri, StringComparison.Ordinal))) { - builder.Append(','); + continue; } - builder.Append(' '); - builder.Append(Parameters.Error); - builder.Append("=\""); - builder.Append(context.Response.Error.Replace("\"", "\\\"")); - builder.Append('"'); - } - - // Append the error_description if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error)) + // Ignore values that can't be represented as unique strings. + var value = (string) parameter.Value; + if (string.IsNullOrEmpty(value)) { - builder.Append(','); + continue; } + parameters[parameter.Key] = value; + } + + var builder = new StringBuilder(scheme); + + foreach (var parameter in parameters) + { builder.Append(' '); - builder.Append(Parameters.ErrorDescription); - builder.Append("=\""); - builder.Append(context.Response.ErrorDescription.Replace("\"", "\\\"")); + builder.Append(parameter.Key); + builder.Append('='); + builder.Append('"'); + builder.Append(parameter.Value.Replace("\"", "\\\"")); builder.Append('"'); + builder.Append(','); } - // Append the error_uri if one was specified. - if (!string.IsNullOrEmpty(context.Response.ErrorUri)) + // If the WWW-Authenticate header ends with a comma, remove it. + if (builder[builder.Length - 1] == ',') { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription)) - { - builder.Append(','); - } + builder.Remove(builder.Length - 1, 1); + } - builder.Append(' '); - builder.Append(Parameters.ErrorUri); - builder.Append("=\""); - builder.Append(context.Response.ErrorUri.Replace("\"", "\\\"")); - builder.Append('"'); + response.Headers.Append("WWW-Authenticate", builder.ToString()); + + return default; + } + } + + /// + /// Contains the logic responsible of processing challenge responses that contain a WWW-Authenticate header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ProcessChallengeErrorResponse : IOpenIddictValidationHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - // Append the scope if one was specified. - if (!string.IsNullOrEmpty(context.Response.Scope)) + // 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) { - if (!string.IsNullOrEmpty(context.Response.Realm) || - !string.IsNullOrEmpty(context.Response.Error) || - !string.IsNullOrEmpty(context.Response.ErrorDescription) || - !string.IsNullOrEmpty(context.Response.ErrorUri)) - { - builder.Append(','); - } + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } - builder.Append(' '); - builder.Append(Parameters.Scope); - builder.Append("=\""); - builder.Append(context.Response.Scope.Replace("\"", "\\\"")); - builder.Append('"'); + // If the response doesn't contain a WWW-Authenticate header, don't return an empty response. + if (!response.Headers.ContainsKey("WWW-Authenticate")) + { + return default; } - response.Headers.Append("WWW-Authenticate", builder.ToString()); + context.Logger.LogInformation("The response was successfully returned as an empty challenge response."); + context.HandleRequest(); return default; } @@ -575,11 +582,6 @@ namespace OpenIddict.Validation.Owin 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 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; diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs index b13b9e09..f7b86f39 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs @@ -19,5 +19,11 @@ namespace OpenIddict.Validation.Owin public OpenIddictValidationOwinOptions() : base(OpenIddictValidationOwinDefaults.AuthenticationType) => AuthenticationMode = AuthenticationMode.Passive; + + /// + /// Gets or sets the optional "realm" value returned to + /// the caller as part of the WWW-Authenticate header. + /// + public string Realm { get; set; } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 91f6806c..e4fe851e 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -511,21 +511,6 @@ namespace Microsoft.Extensions.DependencyInjection return SetIssuer(uri); } - /// - /// Sets the realm returned to the caller as part of challenge responses. - /// - /// The issuer address. - /// The . - public OpenIddictValidationBuilder SetRealm([NotNull] string realm) - { - if (string.IsNullOrEmpty(realm)) - { - throw new ArgumentException("The realm cannot be null or empty.", nameof(realm)); - } - - return Configure(options => options.Realm = realm); - } - /// /// Configures OpenIddict to use introspection instead of local/direct validation. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 11c91518..1e99a71d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -800,8 +800,6 @@ namespace OpenIddict.Validation context.Response.ErrorDescription = "The user represented by the token is not allowed to perform the requested action."; } - context.Response.Realm = context.Options.Realm; - return default; } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 5abf0915..68723734 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -108,12 +108,6 @@ namespace OpenIddict.Validation /// public ISet Audiences { get; } = new HashSet(StringComparer.Ordinal); - /// - /// Gets or sets the optional "realm" value returned to - /// the caller as part of challenge responses. - /// - public string Realm { get; set; } - /// /// Gets the token validation parameters used by the OpenIddict validation services. /// diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs index 0af2cdad..a96c5596 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs @@ -71,13 +71,6 @@ namespace OpenIddict.Abstractions.Tests.Primitives /* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4") }; - yield return new object[] - { - /* property: */ nameof(OpenIddictResponse.Realm), - /* name: */ OpenIddictConstants.Parameters.Realm, - /* value: */ new OpenIddictParameter("802A3E3E-DCCA-4EFC-89FA-7D82FE8C27E4") - }; - yield return new object[] { /* property: */ nameof(OpenIddictResponse.RefreshToken), diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs index 6d8d96f0..d2efaa09 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs @@ -312,7 +312,45 @@ namespace OpenIddict.Server.FunctionalTests private async Task GetResponseAsync(HttpResponseMessage message) { - if (message.Headers.Location != null) + if (message.Headers.WwwAuthenticate.Count != 0) + { + var response = new OpenIddictResponse(); + + foreach (var header in message.Headers.WwwAuthenticate) + { + if (string.IsNullOrEmpty(header.Parameter)) + { + continue; + } + + foreach (var parameter in header.Parameter.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + var values = parameter.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + if (values.Length != 2) + { + continue; + } + + var name = values[0]?.Trim(' ', '"'); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + var value = values[1]?.Trim(' ', '"'); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + response.SetParameter(name, value); + } + } + + return response; + } + + else if (message.Headers.Location != null) { var payload = message.Headers.Location.Fragment; if (string.IsNullOrEmpty(payload)) @@ -325,7 +363,7 @@ namespace OpenIddict.Server.FunctionalTests return new OpenIddictResponse(); } - string UnescapeDataString(string value) + static string UnescapeDataString(string value) { if (string.IsNullOrEmpty(value)) { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs index 5fc83246..99d0666f 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs @@ -137,8 +137,7 @@ namespace OpenIddict.Server.FunctionalTests }); // Assert - Assert.Equal(Errors.MissingToken, response.Error); - Assert.Equal("The mandatory access token is missing.", response.ErrorDescription); + Assert.Empty(response.GetParameters()); } [Fact] @@ -721,7 +720,7 @@ namespace OpenIddict.Server.FunctionalTests // Act var response = await client.PostAsync("/connect/userinfo", new OpenIddictRequest { - Token = "SlAV32hkKG" + AccessToken = "SlAV32hkKG" }); // Assert @@ -736,6 +735,21 @@ namespace OpenIddict.Server.FunctionalTests { options.EnableDegradedMode(); + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SlAV32hkKG", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + options.AddEventHandler(builder => builder.UseInlineHandler(context => { @@ -753,7 +767,7 @@ namespace OpenIddict.Server.FunctionalTests // Act var response = await client.PostAsync("/connect/userinfo", new OpenIddictRequest { - Token = "SlAV32hkKG" + AccessToken = "SlAV32hkKG" }); // Assert