From 94f1189592027175d41ead30d800dafd9aeeb6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 20 Aug 2016 14:01:48 +0200 Subject: [PATCH] Remove OpenIddictMiddleware and the parameterless OpenIddictBuilder.EnableAuthorizationEndpoint() overloads --- README.md | 64 ++++++- .../Controllers/AuthorizationController.cs | 54 +++++- .../Infrastructure/OpenIddictMiddleware.cs | 180 ------------------ .../OpenIddictProvider.Exchange.cs | 8 - src/OpenIddict.Core/OpenIddictBuilder.cs | 60 +----- src/OpenIddict.Core/OpenIddictDefaults.cs | 44 ----- src/OpenIddict.Core/OpenIddictExtensions.cs | 24 +-- 7 files changed, 115 insertions(+), 319 deletions(-) delete mode 100644 src/OpenIddict.Core/Infrastructure/OpenIddictMiddleware.cs delete mode 100644 src/OpenIddict.Core/OpenIddictDefaults.cs diff --git a/README.md b/README.md index 5f9e7046..8c0fda07 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,61 @@ public class ApplicationDbContext : IdentityDbContext { services.AddOpenIddict, ApplicationDbContext, int>() ``` -## Enabling interactive flows support + - **Create your own authorization controller**: + +To **support the password or the client credentials flow, you must provide your own token endpoint action**: + +```csharp +[HttpPost("~/connect/token")] +public async Task Exchange() { + var request = HttpContext.GetOpenIdConnectRequest(); + + if (request.IsPasswordGrantType()) { + var user = await _userManager.FindByNameAsync(request.Username); + if (user == null) { + return Json(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant + }); + } + + // Ensure the password is valid. + if (!await _userManager.CheckPasswordAsync(user, request.Password)) { + if (_userManager.SupportsUserLockout) { + await _userManager.AccessFailedAsync(user); + } + + return Json(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant + }); + } + + if (_userManager.SupportsUserLockout) { + await _userManager.ResetAccessFailedCountAsync(user); + } + + var identity = await _userManager.CreateIdentityAsync(user, request.GetScopes()); + + // Create a new authentication ticket holding the user identity. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetResources(request.GetResources()); + ticket.SetScopes(request.GetScopes()); + + return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + } + + return Json(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.UnsupportedGrantType + }); +} +``` + +To **enable authorization code/implicit flows support, you'll similarly have to create your own authorization endpoint action** and your own views/view models. The Mvc.Server sample comes with an [`AuthorizationController` that you can easily reuse in your application](https://github.com/openiddict/openiddict-core/blob/dev/samples/Mvc.Server/Controllers/AuthorizationController.cs). + +![](https://cloud.githubusercontent.com/assets/6998306/10988233/d9026712-843a-11e5-8ff0-e7addffd727b.png) - **Enable the corresponding flows in the OpenIddict options**: @@ -175,14 +229,6 @@ public void ConfigureServices(IServiceCollection services) { } ``` - - **Create your own authorization controller and your own views**: - -**By default, OpenIddict processes authorization requests without requiring user consent**, which allows you to use OpenIddict with your own SPA apps without having to add custom code. - -**To display a confirmation form, you must create your own controller** and your own views/view models. The Mvc.Server sample comes with an [`AuthorizationController` that you can easily reuse in your application](https://github.com/openiddict/openiddict-core/blob/dev/samples/Mvc.Server/Controllers/AuthorizationController.cs). - -![](https://cloud.githubusercontent.com/assets/6998306/10988233/d9026712-843a-11e5-8ff0-e7addffd727b.png) - - **Register your client application**: ```csharp diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 3063437d..9aa9cd8b 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -36,9 +36,6 @@ namespace Mvc.Server { _userManager = userManager; } - // Note: if you don't provide your own authorization action, OpenIddict will - // directly process authorization requests without requiring user consent. - [Authorize, HttpGet, Route("~/connect/authorize")] public async Task Authorize() { // Extract the authorization request from the ASP.NET environment. @@ -100,9 +97,6 @@ namespace Mvc.Server { return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme); } - // Note: if you don't provide your own logout action, OpenIddict will - // directly process logout requests without requiring user confirmation. - [HttpGet("~/connect/logout")] public IActionResult Logout() { // Extract the authorization request from the ASP.NET environment. @@ -126,5 +120,53 @@ namespace Mvc.Server { // to the post_logout_redirect_uri specified by the client application. return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme); } + + // Note: to support the password grant type, you must provide your own token endpoint action: + + // [HttpPost("~/connect/token")] + // public async Task Exchange() { + // var request = HttpContext.GetOpenIdConnectRequest(); + // + // if (request.IsPasswordGrantType()) { + // var user = await _userManager.FindByNameAsync(request.Username); + // if (user == null) { + // return Json(new OpenIdConnectResponse { + // Error = OpenIdConnectConstants.Errors.InvalidGrant + // }); + // } + // + // // Ensure the password is valid. + // if (!await _userManager.CheckPasswordAsync(user, request.Password)) { + // if (_userManager.SupportsUserLockout) { + // await _userManager.AccessFailedAsync(user); + // } + // + // return Json(new OpenIdConnectResponse { + // Error = OpenIdConnectConstants.Errors.InvalidGrant + // }); + // } + // + // if (_userManager.SupportsUserLockout) { + // await _userManager.ResetAccessFailedCountAsync(user); + // } + // + // var identity = await _userManager.CreateIdentityAsync(user, request.GetScopes()); + // + // // Create a new authentication ticket holding the user identity. + // var ticket = new AuthenticationTicket( + // new ClaimsPrincipal(identity), + // new AuthenticationProperties(), + // OpenIdConnectServerDefaults.AuthenticationScheme); + // + // ticket.SetResources(request.GetResources()); + // ticket.SetScopes(request.GetScopes()); + // + // return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + // } + // + // return Json(new OpenIdConnectResponse { + // Error = OpenIdConnectConstants.Errors.UnsupportedGrantType + // }); + // } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictMiddleware.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictMiddleware.cs deleted file mode 100644 index aad8aa2d..00000000 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictMiddleware.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Diagnostics; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace OpenIddict.Infrastructure { - public class OpenIddictMiddleware - where TUser : class where TApplication : class - where TAuthorization : class where TScope : class where TToken : class { - private readonly RequestDelegate next; - - public OpenIddictMiddleware([NotNull] RequestDelegate next) { - this.next = next; - } - - public async Task Invoke([NotNull] HttpContext context) { - // Invoke the rest of the pipeline to allow handling - // authorization, logout or token requests in user code. - await next(context); - - // If the request was already handled, skip the default logic. - if (context.Response.HasStarted || context.Response.StatusCode != 404) { - return; - } - - // If the request doesn't correspond to an OpenID Connect request, ignore it. - var request = context.GetOpenIdConnectRequest(); - if (request == null || (!request.IsAuthorizationRequest() && - !request.IsLogoutRequest() && - !request.IsTokenRequest())) { - return; - } - - // If an OpenID Connect response was already prepared, bypass the default logic. - var response = context.GetOpenIdConnectResponse(); - if (response != null) { - return; - } - - // Resolve the OpenIddict services from the scoped container. - var services = context.RequestServices.GetRequiredService>(); - - // Reset the response status code to allow the OpenID Connect server - // middleware to apply a challenge, signin or logout response. - context.Response.StatusCode = 200; - - ClaimsPrincipal principal = null; - - if (request.IsAuthorizationRequest()) { - // If the user is not logged in, return a challenge response. - if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { - await context.Authentication.ChallengeAsync(); - - return; - } - - // Retrieve the profile of the logged in user. If the user - // cannot be found, return a challenge response. - var user = await services.Users.GetUserAsync(context.User); - if (user == null) { - await context.Authentication.ChallengeAsync(); - - return; - } - - services.Logger.LogInformation("The authorization request was handled without asking for user consent."); - - principal = new ClaimsPrincipal(await services.Users.CreateIdentityAsync(user, request.GetScopes())); - } - - else if (request.IsLogoutRequest()) { - // Ask ASP.NET Core Identity to delete the local and external cookies created - // when the user agent is redirected from the external identity provider - // after a successful authentication flow (e.g Google or Facebook). - await services.SignIn.SignOutAsync(); - - await context.Authentication.SignOutAsync(services.Options.AuthenticationScheme); - - services.Logger.LogInformation("The logout request was handled without asking for user consent."); - - return; - } - - else if (request.IsTokenRequest()) { - Debug.Assert(request.IsClientCredentialsGrantType() || request.IsPasswordGrantType(), - "Only grant_type=client_credentials and grant_type=password requests should be handled here."); - - services.Logger.LogInformation("The token request was automatically handled."); - - if (request.IsClientCredentialsGrantType()) { - // Retrieve the application details corresponding to the requested client_id. - // Note: this call shouldn't return a null instance, but a race condition may occur - // if the application was removed after the initial check made by ValidateTokenRequest. - var application = await services.Applications.FindByClientIdAsync(request.ClientId); - if (application == null) { - services.Logger.LogError("The token request was aborted because the client application " + - "was not found in the database: '{ClientId}'.", request.ClientId); - - await context.Authentication.ForbidAsync(services.Options.AuthenticationScheme); - - return; - } - - var identity = new ClaimsIdentity(services.Options.AuthenticationScheme); - - // Note: the name identifier is always included in both identity and - // access tokens, even if an explicit destination is not specified. - identity.AddClaim(ClaimTypes.NameIdentifier, request.ClientId); - - identity.AddClaim(ClaimTypes.Name, await services.Applications.GetDisplayNameAsync(application), - OpenIdConnectConstants.Destinations.AccessToken, - OpenIdConnectConstants.Destinations.IdentityToken); - - principal = new ClaimsPrincipal(identity); - } - - else if (request.IsPasswordGrantType()) { - // Retrieve the user profile corresponding to the specified username. - var user = await services.Users.FindByNameAsync(request.Username); - if (user == null) { - services.Logger.LogError("The token request was rejected because no user profile corresponding to " + - "the specified username was found: '{Username}'.", request.Username); - - await context.Authentication.ForbidAsync(services.Options.AuthenticationScheme); - - return; - } - - // Ensure the username/password couple is valid. - if (!await services.Users.CheckPasswordAsync(user, request.Password)) { - services.Logger.LogError("The token request was rejected because the password didn't match " + - "the password associated with the account '{Username}'.", request.Username); - - if (services.Users.SupportsUserLockout) { - await services.Users.AccessFailedAsync(user); - - if (await services.Users.IsLockedOutAsync(user)) { - services.Logger.LogError("The token request was rejected because the account '{Username}' " + - "was locked out to prevent brute force attacks.", request.Username); - } - } - - await context.Authentication.ForbidAsync(services.Options.AuthenticationScheme); - - return; - } - - if (services.Users.SupportsUserLockout) { - await services.Users.ResetAccessFailedCountAsync(user); - } - - principal = new ClaimsPrincipal(await services.Users.CreateIdentityAsync(user, request.GetScopes())); - } - } - - // At this stage, don't alter the response - // if a sign-in operation can't be performed. - if (principal != null) { - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket( - principal, new AuthenticationProperties(), - services.Options.AuthenticationScheme); - - ticket.SetResources(request.GetResources()); - ticket.SetScopes(request.GetScopes()); - - await context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); - } - } - } -} diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index 00aca09e..3b4c9a20 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -401,10 +401,6 @@ namespace OpenIddict.Infrastructure { } } - // Call context.SkipToNextMiddleware() to invoke the next middleware in the pipeline. - // This allows handling grant_type=password requests in a custom controller action. - // If the request is not handled in user code, OpenIddictMiddleware will automatically - // create and return a token response using the default authentication logic. context.SkipToNextMiddleware(); } @@ -414,10 +410,6 @@ namespace OpenIddict.Infrastructure { Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId) && !string.IsNullOrEmpty(context.Request.ClientSecret), "The client credentials shouldn't be null."); - // Call context.SkipToNextMiddleware() to invoke the next middleware in the pipeline. - // This allows handling grant_type=client_credentials requests in a custom controller action. - // If the request is not handled in user code, OpenIddictMiddleware will automatically - // create and return a token response using the default authentication logic. context.SkipToNextMiddleware(); } } diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index 3a7a8430..5be62344 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -506,15 +506,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Enables the authorization endpoint using the default endpoint path (/connect/authorize). - /// - /// The . - public virtual OpenIddictBuilder EnableAuthorizationEndpoint() { - return EnableAuthorizationEndpoint(OpenIddictDefaults.AuthorizationEndpointPath); - } - - /// - /// Enables the authorization endpoint using the specified path. + /// Enables the authorization endpoint. /// /// The relative path of the authorization endpoint. /// The . @@ -523,15 +515,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Enables the introspection endpoint using the default endpoint path (/connect/introspect). - /// - /// The . - public virtual OpenIddictBuilder EnableIntrospectionEndpoint() { - return EnableIntrospectionEndpoint(OpenIddictDefaults.IntrospectionEndpointPath); - } - - /// - /// Enables the introspection endpoint using the specified path. + /// Enables the introspection endpoint. /// /// The relative path of the logout endpoint. /// The . @@ -540,15 +524,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Enables the logout endpoint using the default endpoint path (/connect/logout). - /// - /// The . - public virtual OpenIddictBuilder EnableLogoutEndpoint() { - return EnableLogoutEndpoint(OpenIddictDefaults.LogoutEndpointPath); - } - - /// - /// Enables the logout endpoint using the specified path. + /// Enables the logout endpoint. /// /// The relative path of the logout endpoint. /// The . @@ -557,15 +533,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Enables the revocation endpoint using the default endpoint path (/connect/revoke). - /// - /// The . - public virtual OpenIddictBuilder EnableRevocationEndpoint() { - return EnableRevocationEndpoint(OpenIddictDefaults.RevocationEndpointPath); - } - - /// - /// Enables the revocation endpoint using the specified path. + /// Enables the revocation endpoint. /// /// The relative path of the revocation endpoint. /// The . @@ -574,15 +542,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Enables the token endpoint using the default endpoint path (/connect/token). - /// - /// The . - public virtual OpenIddictBuilder EnableTokenEndpoint() { - return EnableTokenEndpoint(OpenIddictDefaults.TokenEndpointPath); - } - - /// - /// Enables the token endpoint using the specified path. + /// Enables the token endpoint. /// /// The relative path of the token endpoint. /// The . @@ -591,15 +551,7 @@ namespace Microsoft.AspNetCore.Builder { } /// - /// Enables the userinfo endpoint using the default endpoint path (/connect/userinfo). - /// - /// The . - public virtual OpenIddictBuilder EnableUserinfoEndpoint() { - return EnableUserinfoEndpoint(OpenIddictDefaults.UserinfoEndpointPath); - } - - /// - /// Enables the userinfo endpoint using the specified path. + /// Enables the userinfo endpoint. /// /// The relative path of the userinfo endpoint. /// The . diff --git a/src/OpenIddict.Core/OpenIddictDefaults.cs b/src/OpenIddict.Core/OpenIddictDefaults.cs deleted file mode 100644 index 62cd235b..00000000 --- a/src/OpenIddict.Core/OpenIddictDefaults.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using AspNet.Security.OpenIdConnect.Server; - -namespace OpenIddict { - /// - /// Exposes the default values used by OpenIddict. - /// - public static class OpenIddictDefaults { - /// - /// Default value for . - /// - public const string AuthorizationEndpointPath = "/connect/authorize"; - - /// - /// Default value for . - /// - public const string IntrospectionEndpointPath = "/connect/introspect"; - - /// - /// Default value for . - /// - public const string LogoutEndpointPath = "/connect/logout"; - - /// - /// Default value for . - /// - public const string RevocationEndpointPath = "/connect/revoke"; - - /// - /// Default value for . - /// - public const string TokenEndpointPath = "/connect/token"; - - /// - /// Default value for . - /// - public const string UserinfoEndpointPath = "/connect/userinfo"; - } -} diff --git a/src/OpenIddict.Core/OpenIddictExtensions.cs b/src/OpenIddict.Core/OpenIddictExtensions.cs index 5a2debfa..00b5c240 100644 --- a/src/OpenIddict.Core/OpenIddictExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictExtensions.cs @@ -7,7 +7,6 @@ using System; using System.Linq; using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -64,16 +63,6 @@ namespace Microsoft.AspNetCore.Builder { builder.Configure(options => { // Register the OpenID Connect server provider in the OpenIddict options. options.Provider = new OpenIddictProvider(); - - // Register the OpenID Connect server middleware as a native module. - options.Modules.Add(new OpenIddictModule("OpenID Connect server", 0, app => { - app.UseMiddleware(); - })); - - // Register the OpenIddict middleware as a built-in module. - options.Modules.Add(new OpenIddictModule("OpenIddict", 1, app => { - app.UseMiddleware>(); - })); }); // Register the OpenIddict core services in the DI container. @@ -84,12 +73,6 @@ namespace Microsoft.AspNetCore.Builder { builder.Services.TryAddScoped>(); builder.Services.TryAddScoped>(); - // Override the default options manager for IOptions - // to ensure the OpenID Connect server middleware uses the OpenIddict options. - builder.Services.TryAddScoped>(provider => { - return provider.GetRequiredService>(); - }); - return builder; } @@ -149,8 +132,13 @@ namespace Microsoft.AspNetCore.Builder { "client credentials, password and refresh token flows."); } + // Get the modules registered by the application + // and add the OpenID Connect server middleware. + var modules = options.Modules.ToList(); + modules.Add(new OpenIddictModule("OpenID Connect server", 0, builder => builder.UseOpenIdConnectServer(options))); + // Register the OpenIddict modules in the ASP.NET Core pipeline. - foreach (var module in options.Modules.OrderBy(module => module.Position)) { + foreach (var module in modules.OrderBy(module => module.Position)) { if (module?.Registration == null) { throw new InvalidOperationException("An invalid OpenIddict module was registered."); }