Browse Source

Update the validation OWIN integration to support active authentication and rework how errors are returned for API requests

pull/957/head
Kévin Chalet 6 years ago
parent
commit
2ac5b4b884
  1. 9
      src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
  2. 15
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
  3. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  4. 42
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  5. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs
  6. 177
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  7. 6
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs
  8. 15
      src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
  9. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  10. 42
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  11. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs
  12. 171
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  13. 6
      src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs
  14. 15
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  15. 14
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  16. 6
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  17. 15
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs
  18. 1
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs
  19. 28
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs
  20. 175
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs
  21. 5
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs
  22. 31
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinBuilder.cs
  23. 1
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs
  24. 33
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  25. 174
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs
  26. 6
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs
  27. 15
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  28. 2
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  29. 6
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  30. 7
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictResponseTests.cs
  31. 42
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs
  32. 22
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs

9
src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs

@ -150,15 +150,6 @@ namespace OpenIddict.Abstractions
set => SetParameter(OpenIddictConstants.Parameters.IdToken, value);
}
/// <summary>
/// Gets or sets the "realm" parameter.
/// </summary>
public string Realm
{
get => (string) GetParameter(OpenIddictConstants.Parameters.Realm);
set => SetParameter(OpenIddictConstants.Parameters.Realm, value);
}
/// <summary>
/// Gets or sets the "refresh_token" parameter.
/// </summary>

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

@ -146,6 +146,21 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerAspNetCoreBuilder EnableStatusCodePagesIntegration()
=> Configure(options => options.EnableStatusCodePagesIntegration = true);
/// <summary>
/// Sets the realm returned to the caller as part of the WWW-Authenticate header.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/>.</returns>
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);
}
/// <summary>
/// Sets the caching policy used by the authorization endpoint.
/// Note: the specified policy is only used when caching is explicitly enabled.

1
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";
}
}

42
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -43,7 +43,7 @@ namespace OpenIddict.Server.AspNetCore
: base(options, logger, encoder, clock)
=> _provider = provider;
public async Task<bool> 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<bool> HandleRequestAsync()
{
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction ??
throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context.");
var context = transaction.GetProperty<ProcessRequestContext>(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<AuthenticateResult> HandleAuthenticateAsync()
{
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.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<OpenIddictServerAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.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<OpenIddictServerAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.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<OpenIddictServerAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction ??
throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context.");
var context = new ProcessSignOutContext(transaction)
{

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs

@ -31,6 +31,7 @@ namespace OpenIddict.Server.AspNetCore
*/
AttachHttpResponseCode<ApplyUserinfoResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyUserinfoResponseContext>.Descriptor,
ProcessChallengeErrorResponse<ApplyUserinfoResponseContext>.Descriptor,
ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor);
}
}

177
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
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;
public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor<OpenIddictServerAspNetCoreOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -999,7 +1000,7 @@ namespace OpenIddict.Server.AspNetCore
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.SetOrder(ProcessChallengeErrorResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
@ -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<string, string>(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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessChallengeErrorResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessChallengeErrorResponse<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;

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

@ -93,6 +93,12 @@ namespace OpenIddict.Server.AspNetCore
/// </summary>
public bool EnableStatusCodePagesIntegration { get; set; }
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of the WWW-Authenticate header.
/// </summary>
public string Realm { get; set; }
/// <summary>
/// Gets or sets the caching policy used by the authorization endpoint.
/// </summary>

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

@ -138,6 +138,21 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerOwinBuilder EnableLogoutEndpointCaching()
=> Configure(options => options.EnableLogoutEndpointCaching = true);
/// <summary>
/// Sets the realm returned to the caller as part of the WWW-Authenticate header.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/>.</returns>
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);
}
/// <summary>
/// Sets the caching policy used by the authorization endpoint.
/// Note: the specified policy is only used when caching is explicitly enabled.

1
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";
}
}

42
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<bool> 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<bool> InvokeAsync()
{
var transaction = Context.Get<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName) ??
throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict server context.");
var context = transaction.GetProperty<ProcessRequestContext>(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<OpenIddictServerTransaction>(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<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName);
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Get<OpenIddictServerTransaction>(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<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName);
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Get<OpenIddictServerTransaction>(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<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName);
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Get<OpenIddictServerTransaction>(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();

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs

@ -31,6 +31,7 @@ namespace OpenIddict.Server.Owin
*/
AttachHttpResponseCode<ApplyUserinfoResponseContext>.Descriptor,
AttachWwwAuthenticateHeader<ApplyUserinfoResponseContext>.Descriptor,
ProcessChallengeErrorResponse<ApplyUserinfoResponseContext>.Descriptor,
ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor);
}
}

171
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
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;
public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor<OpenIddictServerOwinOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -931,7 +931,7 @@ namespace OpenIddict.Server.Owin
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.SetOrder(ProcessChallengeErrorResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
@ -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<string, string>(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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessChallengeErrorResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessChallengeErrorResponse<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;

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

@ -93,6 +93,12 @@ namespace OpenIddict.Server.Owin
/// </summary>
public bool EnableLogoutEndpointCaching { get; set; }
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of the WWW-Authenticate header.
/// </summary>
public string Realm { get; set; }
/// <summary>
/// Gets or sets the caching policy used by the authorization endpoint.
/// </summary>

15
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1777,21 +1777,6 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.Issuer = address);
}
/// <summary>
/// Sets the realm returned to the caller as part of challenge responses.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
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);
}
/// <summary>
/// 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).

14
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;
}
}

6
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -330,12 +330,6 @@ namespace OpenIddict.Server
/// </summary>
public bool IgnoreScopePermissions { get; set; }
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of challenge responses.
/// </summary>
public string Realm { get; set; }
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect scopes enabled for this application.
/// </summary>

15
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreBuilder.cs

@ -48,6 +48,21 @@ namespace Microsoft.Extensions.DependencyInjection
return this;
}
/// <summary>
/// Sets the realm returned to the caller as part of the WWW-Authenticate header.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationAspNetCoreBuilder"/>.</returns>
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);
}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>

1
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";
}
}

28
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs

@ -40,7 +40,7 @@ namespace OpenIddict.Validation.AspNetCore
: base(options, logger, encoder, clock)
=> _provider = provider;
public async Task<bool> 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<bool> HandleRequestAsync()
{
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction ??
throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context.");
var context = transaction.GetProperty<ProcessRequestContext>(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<AuthenticateResult> HandleAuthenticateAsync()
{
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.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<OpenIddictValidationAspNetCoreFeature>()?.Transaction;
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction ??
throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context.");
transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties();

175
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<ProcessChallengeContext>.Descriptor,
AttachCacheControlHeader<ProcessChallengeContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessChallengeContext>.Descriptor,
ProcessChallengeErrorResponse<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessChallengeErrorResponse<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <summary>
@ -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
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOptionsMonitor<OpenIddictValidationAspNetCoreOptions> _options;
public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor<OpenIddictValidationAspNetCoreOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -396,7 +400,7 @@ namespace OpenIddict.Validation.AspNetCore
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.SetOrder(ProcessChallengeErrorResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
@ -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<string, string>(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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessChallengeErrorResponse<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessChallengeErrorResponse<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;

5
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreOptions.cs

@ -13,5 +13,10 @@ namespace OpenIddict.Validation.AspNetCore
/// </summary>
public class OpenIddictValidationAspNetCoreOptions : AuthenticationSchemeOptions
{
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of the WWW-Authenticate header.
/// </summary>
public string Realm { get; set; }
}
}

31
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;
}
/// <summary>
/// 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 <see cref="AuthenticationManager.Challenge(string[])"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>The <see cref="OpenIddictValidationOwinBuilder"/>.</returns>
public OpenIddictValidationOwinBuilder UseActiveAuthentication()
=> Configure(options => options.AuthenticationMode = AuthenticationMode.Active);
/// <summary>
/// Sets the realm returned to the caller as part of the WWW-Authenticate header.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationOwinBuilder"/>.</returns>
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);
}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>

1
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";
}
}

33
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<bool> 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<bool> InvokeAsync()
{
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName) ??
throw new InvalidOperationException("An unknown error occurred while retrieving the OpenIddict validation context.");
var context = transaction.GetProperty<ProcessRequestContext>(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<AuthenticationTicket> AuthenticateCoreAsync()
{
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
if (transaction?.Request == null)
{
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
var transaction = Context.Get<OpenIddictValidationTransaction>(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<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
if (transaction == null)
{
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
var transaction = Context.Get<OpenIddictValidationTransaction>(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();

174
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<ProcessChallengeContext>.Descriptor,
AttachCacheControlHeader<ProcessChallengeContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessChallengeContext>.Descriptor,
ProcessChallengeErrorResponse<ProcessChallengeContext>.Descriptor,
ProcessJsonResponse<ProcessChallengeContext>.Descriptor,
AttachHttpResponseCode<ProcessErrorContext>.Descriptor,
AttachCacheControlHeader<ProcessErrorContext>.Descriptor,
AttachWwwAuthenticateHeader<ProcessErrorContext>.Descriptor,
ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor,
ProcessJsonResponse<ProcessErrorContext>.Descriptor);
/// <summary>
@ -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
/// </summary>
public class AttachWwwAuthenticateHeader<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOptionsMonitor<OpenIddictValidationOwinOptions> _options;
public AttachWwwAuthenticateHeader([NotNull] IOptionsMonitor<OpenIddictValidationOwinOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
@ -398,7 +401,7 @@ namespace OpenIddict.Validation.Owin
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachWwwAuthenticateHeader<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.SetOrder(ProcessChallengeErrorResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
@ -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<string, string>(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;
}
}
/// <summary>
/// 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.
/// </summary>
public class ProcessChallengeErrorResponse<TContext> : IOpenIddictValidationHandler<TContext> where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessChallengeErrorResponse<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;

6
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinOptions.cs

@ -19,5 +19,11 @@ namespace OpenIddict.Validation.Owin
public OpenIddictValidationOwinOptions()
: base(OpenIddictValidationOwinDefaults.AuthenticationType)
=> AuthenticationMode = AuthenticationMode.Passive;
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of the WWW-Authenticate header.
/// </summary>
public string Realm { get; set; }
}
}

15
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -511,21 +511,6 @@ namespace Microsoft.Extensions.DependencyInjection
return SetIssuer(uri);
}
/// <summary>
/// Sets the realm returned to the caller as part of challenge responses.
/// </summary>
/// <param name="realm">The issuer address.</param>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
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);
}
/// <summary>
/// Configures OpenIddict to use introspection instead of local/direct validation.
/// </summary>

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

6
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -108,12 +108,6 @@ namespace OpenIddict.Validation
/// </summary>
public ISet<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the optional "realm" value returned to
/// the caller as part of challenge responses.
/// </summary>
public string Realm { get; set; }
/// <summary>
/// Gets the token validation parameters used by the OpenIddict validation services.
/// </summary>

7
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),

42
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTestClient.cs

@ -312,7 +312,45 @@ namespace OpenIddict.Server.FunctionalTests
private async Task<OpenIddictResponse> 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))
{

22
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<ProcessAuthenticationContext>(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<ApplyUserinfoResponseContext>(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

Loading…
Cancel
Save