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