diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index 30ce56ff..c9fbad6f 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -57,13 +56,29 @@ public class AuthorizationController : Controller var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - // Retrieve the user principal stored in the authentication cookie. - // If a max_age parameter was provided, ensure that the cookie is not too old. - // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page. + // Try to retrieve the user principal stored in the authentication cookie and redirect + // 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. + // - 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); - if (result?.Identity == null || (request.MaxAge != null && result.Properties?.IssuedUtc != null && - TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) + if (result is not { Identity: ClaimsIdentity } || + ((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 // that will be used to authenticate the user, the identity_provider parameter can be used for that. if (!string.IsNullOrEmpty(request.IdentityProvider)) diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs index 48be99da..5269c2e7 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs +++ b/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 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. // // 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. var result = await HttpContext.AuthenticateAsync(); - if (result == null || !result.Succeeded || request.HasPromptValue(PromptValues.Login) || - (request.MaxAge != null && result.Properties?.IssuedUtc != null && - TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) + if (result is not { Succeeded: true } || + ((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)) { // If the client application requested promptless authentication, // 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 - // is removed from the authorization request payload before redirecting the user. - var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login)); - - var parameters = Request.HasFormContentType ? - Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : - Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); - - parameters.Add(new(Parameters.Prompt, new StringValues(prompt))); + // 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 Core + // 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 // 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, 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, @@ -140,7 +143,8 @@ public class AuthorizationController : Controller // authentication options shouldn't be used, a specific scheme can be specified here. 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) }); } diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs index ef2420e5..579c5b54 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs @@ -38,8 +38,8 @@ public static class OpenIddictMongoDbExtensions .SetDefaultScopeEntity() .SetDefaultTokenEntity(); - // Note: the Mongo stores/resolvers don't depend on scoped/transient services and thus - // can be safely registered as singleton services and shared/reused across requests. + // Note: the Mongo stores don't depend on scoped/transient services and thus can + // be safely registered as singleton services and shared/reused across requests. builder.ReplaceApplicationStore(ServiceLifetime.Singleton) .ReplaceAuthorizationStore(ServiceLifetime.Singleton) .ReplaceScopeStore(ServiceLifetime.Singleton) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index a71918ed..e445d250 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -6,7 +6,6 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Globalization; using System.Security.Claims; using System.Text.Json; 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 // 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.RedirectUri = request.RedirectUri;