From 34fa0c1eb5f9652860e3540a571b68456e732a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 23 Jun 2016 21:25:36 +0200 Subject: [PATCH] Implement POST authorization requests support using user sessions --- samples/Mvc.Client/Startup.cs | 2 + samples/Mvc.Server/Startup.cs | 4 + samples/Mvc.Server/project.json | 1 + .../OpenIddictProvider.Authentication.cs | 94 ++++++++++++++++++- src/OpenIddict.Core/OpenIddictConstants.cs | 4 + src/OpenIddict.Mvc/OpenIddictController.cs | 22 +---- 6 files changed, 106 insertions(+), 21 deletions(-) diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 48e3ccef..036b083d 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -40,6 +41,7 @@ namespace Mvc.Client { // Use the authorization code flow. ResponseType = OpenIdConnectResponseType.Code, + AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet, // Note: setting the Authority allows the OIDC client middleware to automatically // retrieve the identity provider's configuration and spare you from setting diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 14334710..e79b4ba6 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -22,6 +22,8 @@ namespace Mvc.Server { services.AddMvc(); + services.AddSession(); + services.AddEntityFramework() .AddEntityFrameworkSqlServer() .AddDbContext(options => @@ -120,6 +122,8 @@ namespace Mvc.Server { app.UseSession(); + app.UseSession(); + app.UseOpenIddict(); app.UseMvcWithDefaultRoute(); diff --git a/samples/Mvc.Server/project.json b/samples/Mvc.Server/project.json index 39c85d93..27bcf95b 100644 --- a/samples/Mvc.Server/project.json +++ b/samples/Mvc.Server/project.json @@ -27,6 +27,7 @@ "Microsoft.AspNetCore.Mvc": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Session": "1.0.0", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.Extensions.Configuration.CommandLine": "1.0.0", diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index ea2c1748..b6eb6b8b 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -13,14 +13,18 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class where TAuthorization : class where TScope : class where TToken : class { - public override Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context) { + public override async Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); // Reject requests using the unsupported request parameter. @@ -32,7 +36,7 @@ namespace OpenIddict.Infrastructure { error: OpenIdConnectConstants.Errors.RequestNotSupported, description: "The request parameter is not supported."); - return Task.FromResult(0); + return; } // Reject requests using the unsupported request_uri parameter. @@ -44,10 +48,43 @@ namespace OpenIddict.Infrastructure { error: OpenIdConnectConstants.Errors.RequestUriNotSupported, description: "The request_uri parameter is not supported."); - return Task.FromResult(0); + return; } - return Task.FromResult(0); + // If a request_id parameter can be found in the authorization request, + // restore the complete authorization request stored in the user session. + if (!string.IsNullOrEmpty(context.Request.GetRequestId())) { + // Ensure session support has been enabled for this request. + if (context.HttpContext.Features.Get() == null) { + services.Logger.LogError("The authorization request was rejected because the session middleware " + + "was not correctly registered. Session support must be enabled to allow " + + "processing authorization requests specifying a request_id parameter."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The request_id parameter is not allowed by this authorization server."); + + return; + } + + // Load the session from the session store. + await context.HttpContext.Session.LoadAsync(); + + var payload = context.HttpContext.Session.Get(OpenIddictConstants.Environment.Request + context.Request.GetRequestId()); + if (payload == null) { + services.Logger.LogError("The authorization request was rejected because an unknown " + + "or invalid request_id parameter was specified."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "Invalid request: timeout expired."); + + return; + } + + // Restore the authorization request parameters from the serialized payload. + context.Request.Import(payload); + } } public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) { @@ -279,7 +316,56 @@ namespace OpenIddict.Infrastructure { return; } + if (context.HttpContext.Request.Path == context.Options.AuthorizationEndpointPath && + string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { + // Ensure session support has been enabled for this request. + if (context.HttpContext.Features.Get() == null) { + services.Logger.LogError("The authorization request was rejected because the session middleware " + + "was not correctly registered. Session support must be enabled to allow " + + "processing POST authorization requests."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "POST authorization requests are not allowed by this authorization server."); + + return; + } + + // Generate a request identifier. Note: using a crypto-secure + // random number generator is not necessary in this case. + var identifier = Guid.NewGuid().ToString(); + + // Load the session from the session store. + await context.HttpContext.Session.LoadAsync(); + + // Store the serialized authorization request parameters in the user session. + context.HttpContext.Session.Set(OpenIddictConstants.Environment.Request + identifier, context.Request.Export()); + + // Add the request_id to the current URL. + var address = QueryHelpers.AddQueryString( + uri: context.HttpContext.Request.GetEncodedUrl(), + name: OpenIdConnectConstants.Parameters.RequestId, value: identifier); + + context.HttpContext.Response.Redirect(address); + context.HandleResponse(); + + return; + } + context.SkipToNextMiddleware(); } + + public override Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context) { + // Note: the ApplyAuthorizationResponse event is called for both successful + // and errored authorization responses but discrimination is not necessary here, + // as the authorization request must be removed from the user session in both cases. + + // Remove the authorization request from the user session. + if (!string.IsNullOrEmpty(context.Request.GetRequestId())) { + context.HttpContext.Session.Remove(OpenIddictConstants.Environment.Request + context.Request.GetRequestId()); + } + + return Task.FromResult(0); + } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index a98a3ee7..1f8146a1 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -15,6 +15,10 @@ namespace OpenIddict { public const string Public = "public"; } + public static class Environment { + public const string Request = "openiddict-request:"; + } + public static class Scopes { public const string Roles = "roles"; } diff --git a/src/OpenIddict.Mvc/OpenIddictController.cs b/src/OpenIddict.Mvc/OpenIddictController.cs index 80426ff0..f7d408d4 100644 --- a/src/OpenIddict.Mvc/OpenIddictController.cs +++ b/src/OpenIddict.Mvc/OpenIddictController.cs @@ -5,8 +5,6 @@ */ using System; -using System.Diagnostics; -using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; @@ -23,7 +21,7 @@ namespace OpenIddict.Mvc { // Note: this controller is generic and doesn't need to be marked as internal to prevent MVC from discovering it. public class OpenIddictController : Controller where TUser : class where TApplication : class where TAuthorization : class where TToken : class { - [HttpGet, HttpPost] + [Authorize, HttpGet, HttpPost] public virtual async Task Authorize([FromServices] OpenIddictApplicationManager applications) { // Note: when a fatal error occurs during the request processing, an OpenID Connect response // is prematurely forged and added to the ASP.NET Core context by OpenIdConnectServerHandler. @@ -41,19 +39,6 @@ namespace OpenIddict.Mvc { }); } - // Note: authentication could be theorically enforced at the filter level via AuthorizeAttribute - // but this authorization endpoint accepts both GET and POST requests while the cookie middleware - // only uses 302 responses to redirect the user agent to the login page, making it incompatible with POST. - // To work around this limitation, the OpenID Connect request is automatically saved in the cache and will be - // restored by the OpenID Connect server middleware after the external authentication process has been completed. - if (!User.Identities.Any(identity => identity.IsAuthenticated)) { - return Challenge(new AuthenticationProperties { - RedirectUri = Url.Action(nameof(Authorize), new { - request_id = request.GetRequestId() - }) - }); - } - // Note: the OpenID Connect server middleware automatically ensures an application // corresponds to the client_id specified in the authorization request using // IOpenIdConnectServerProvider.ValidateAuthorizationRequest (see OpenIddictProvider.cs). @@ -98,7 +83,10 @@ namespace OpenIddict.Mvc { // Create a new ClaimsIdentity containing the claims that // will be used to create an id_token, a token or a code. var identity = await users.CreateIdentityAsync(user, request.GetScopes()); - Debug.Assert(identity != null); + if (identity == null) { + throw new InvalidOperationException("The authorization request was aborted because the user manager returned " + + $"a null identity for user '{await users.GetUserNameAsync(user)}'."); + } var application = await applications.FindByClientIdAsync(request.ClientId); if (application == null) {