Browse Source

Stop special-casing the prompt parameter in PAR-enabled authorization requests and update the samples to use TempData to determine whether the user should be redirected to the login endpoint

pull/2290/head
Kévin Chalet 10 months ago
parent
commit
7d90e1b9cb
  1. 27
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  2. 32
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
  3. 4
      src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs
  4. 11
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

27
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs

@ -6,7 +6,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -57,13 +56,29 @@ public class AuthorizationController : Controller
var request = context.GetOpenIddictServerRequest() ?? var request = context.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie. // Try to retrieve the user principal stored in the authentication cookie and redirect
// If a max_age parameter was provided, ensure that the cookie is not too old. // the user agent to the login page (or to an external provider) in the following cases:
// If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. //
// - If the user principal can't be extracted or the cookie is too old.
// - If prompt=login was specified by the client application.
// - If max_age=0 was specified by the client application (max_age=0 is equivalent to prompt=login).
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie); var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
if (result?.Identity == null || (request.MaxAge != null && result.Properties?.IssuedUtc != null && if (result is not { Identity: ClaimsIdentity } ||
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) ((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
TempData["IgnoreAuthenticationChallenge"] is null or false))
{ {
// To avoid endless login endpoint -> authorization endpoint redirects, a special temp data entry is
// used to skip the challenge if the user agent has already been redirected to the login endpoint.
//
// Note: this flag doesn't guarantee that the user has accepted to re-authenticate. If such a guarantee
// is needed, the existing authentication cookie MUST be deleted AND revoked (e.g using ASP.NET
// Identity's security stamp feature with an extremely short revalidation time span) before triggering
// a challenge to redirect the user agent to the login endpoint.
TempData["IgnoreAuthenticationChallenge"] = true;
// For applications that want to allow the client to select the external authentication provider // For applications that want to allow the client to select the external authentication provider
// that will be used to authenticate the user, the identity_provider parameter can be used for that. // that will be used to authenticate the user, the identity_provider parameter can be used for that.
if (!string.IsNullOrEmpty(request.IdentityProvider)) if (!string.IsNullOrEmpty(request.IdentityProvider))

32
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs

@ -71,14 +71,17 @@ public class AuthorizationController : Controller
// //
// - If the user principal can't be extracted or the cookie is too old. // - If the user principal can't be extracted or the cookie is too old.
// - If prompt=login was specified by the client application. // - If prompt=login was specified by the client application.
// - If max_age=0 was specified by the client application (max_age=0 is equivalent to prompt=login).
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough. // - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
// //
// For scenarios where the default authentication handler configured in the ASP.NET Core // For scenarios where the default authentication handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here. // authentication options shouldn't be used, a specific scheme can be specified here.
var result = await HttpContext.AuthenticateAsync(); var result = await HttpContext.AuthenticateAsync();
if (result == null || !result.Succeeded || request.HasPromptValue(PromptValues.Login) || if (result is not { Succeeded: true } ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null && ((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 ||
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
TempData["IgnoreAuthenticationChallenge"] is null or false))
{ {
// If the client application requested promptless authentication, // If the client application requested promptless authentication,
// return an error indicating that the user is not logged in. // return an error indicating that the user is not logged in.
@ -93,15 +96,14 @@ public class AuthorizationController : Controller
})); }));
} }
// To avoid endless login -> authorization redirects, the prompt=login flag // To avoid endless login endpoint -> authorization endpoint redirects, a special temp data entry is
// is removed from the authorization request payload before redirecting the user. // used to skip the challenge if the user agent has already been redirected to the login endpoint.
var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login)); //
// Note: this flag doesn't guarantee that the user has accepted to re-authenticate. If such a guarantee
var parameters = Request.HasFormContentType ? // is needed, the existing authentication cookie MUST be deleted AND revoked (e.g using ASP.NET Core
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : // Identity's security stamp feature with an extremely short revalidation time span) before triggering
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); // a challenge to redirect the user agent to the login endpoint.
TempData["IgnoreAuthenticationChallenge"] = true;
parameters.Add(new(Parameters.Prompt, new StringValues(prompt)));
// For applications that want to allow the client to select the external authentication provider // For applications that want to allow the client to select the external authentication provider
// that will be used to authenticate the user, the identity_provider parameter can be used for that. // that will be used to authenticate the user, the identity_provider parameter can be used for that.
@ -125,7 +127,8 @@ public class AuthorizationController : Controller
provider: request.IdentityProvider, provider: request.IdentityProvider,
redirectUrl: Url.Action("ExternalLoginCallback", "Account", new redirectUrl: Url.Action("ExternalLoginCallback", "Account", new
{ {
ReturnUrl = Request.PathBase + Request.Path + QueryString.Create(parameters) ReturnUrl = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form : Request.Query)
})); }));
// Note: when only one client is registered in the client options, // Note: when only one client is registered in the client options,
@ -140,7 +143,8 @@ public class AuthorizationController : Controller
// authentication options shouldn't be used, a specific scheme can be specified here. // authentication options shouldn't be used, a specific scheme can be specified here.
return Challenge(new AuthenticationProperties return Challenge(new AuthenticationProperties
{ {
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form : Request.Query)
}); });
} }

4
src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs

@ -38,8 +38,8 @@ public static class OpenIddictMongoDbExtensions
.SetDefaultScopeEntity<OpenIddictMongoDbScope>() .SetDefaultScopeEntity<OpenIddictMongoDbScope>()
.SetDefaultTokenEntity<OpenIddictMongoDbToken>(); .SetDefaultTokenEntity<OpenIddictMongoDbToken>();
// Note: the Mongo stores/resolvers don't depend on scoped/transient services and thus // Note: the Mongo stores don't depend on scoped/transient services and thus can
// can be safely registered as singleton services and shared/reused across requests. // be safely registered as singleton services and shared/reused across requests.
builder.ReplaceApplicationStore<OpenIddictMongoDbApplication, OpenIddictMongoDbApplicationStore>(ServiceLifetime.Singleton) builder.ReplaceApplicationStore<OpenIddictMongoDbApplication, OpenIddictMongoDbApplicationStore>(ServiceLifetime.Singleton)
.ReplaceAuthorizationStore<OpenIddictMongoDbAuthorization, OpenIddictMongoDbAuthorizationStore>(ServiceLifetime.Singleton) .ReplaceAuthorizationStore<OpenIddictMongoDbAuthorization, OpenIddictMongoDbAuthorizationStore>(ServiceLifetime.Singleton)
.ReplaceScopeStore<OpenIddictMongoDbScope, OpenIddictMongoDbScopeStore>(ServiceLifetime.Singleton) .ReplaceScopeStore<OpenIddictMongoDbScope, OpenIddictMongoDbScopeStore>(ServiceLifetime.Singleton)

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

@ -6,7 +6,6 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -707,16 +706,6 @@ public static partial class OpenIddictServerHandlers
// For more information, see https://datatracker.ietf.org/doc/html/rfc9101#section-5 and // For more information, see https://datatracker.ietf.org/doc/html/rfc9101#section-5 and
// https://openid.net/specs/openid-connect-core-1_0.html#RequestUriRationale. // https://openid.net/specs/openid-connect-core-1_0.html#RequestUriRationale.
// Note: the prompt parameter is special-cased to allow application code to override the "login" prompt
// value after redirecting the user agent to the login endpoint and asking the user to re-authenticate.
if (request.HasPromptValue(PromptValues.Login) && context.Request.HasParameter(Parameters.Prompt) &&
!context.Request.HasPromptValue(PromptValues.Login))
{
request.Prompt = string.Join(",", request.GetPromptValues()
.ToImmutableHashSet(StringComparer.Ordinal)
.Remove(PromptValues.Login));
}
context.Request = request; context.Request = request;
context.RedirectUri = request.RedirectUri; context.RedirectUri = request.RedirectUri;

Loading…
Cancel
Save