diff --git a/README.md b/README.md index e603b3d7..b69e49f4 100644 --- a/README.md +++ b/README.md @@ -171,19 +171,14 @@ public async Task Exchange() { // Ensure the password is valid. if (!await _userManager.CheckPasswordAsync(user, request.Password)) { - if (_userManager.SupportsUserLockout) { - await _userManager.AccessFailedAsync(user); - } - return BadRequest(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "The username/password couple is invalid." }); } - if (_userManager.SupportsUserLockout) { - await _userManager.ResetAccessFailedCountAsync(user); - } + // Note: for a more complete sample including account lockout support, visit + // https://github.com/openiddict/openiddict-core/blob/dev/samples/Mvc.Server/Controllers/AuthorizationController.cs var identity = await _userManager.CreateIdentityAsync(user, request.GetScopes()); diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index d0fa951f..5aac4a7b 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -36,6 +36,9 @@ namespace Mvc.Server { _userManager = userManager; } + // Note: to support interactive flows like the code flow, + // you must provide your own authorization endpoint action: + [Authorize, HttpGet, Route("~/connect/authorize")] public async Task Authorize() { // Extract the authorization request from the ASP.NET environment. @@ -121,56 +124,81 @@ namespace Mvc.Server { return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme); } - // Note: to support the password grant type, you must provide your own token endpoint action: - - // [HttpPost("~/connect/token")] - // [Produces("application/json")] - // public async Task Exchange() { - // var request = HttpContext.GetOpenIdConnectRequest(); - // - // if (request.IsPasswordGrantType()) { - // var user = await _userManager.FindByNameAsync(request.Username); - // if (user == null) { - // return BadRequest(new OpenIdConnectResponse { - // Error = OpenIdConnectConstants.Errors.InvalidGrant, - // ErrorDescription = "The username/password couple is invalid." - // }); - // } - // - // // Ensure the password is valid. - // if (!await _userManager.CheckPasswordAsync(user, request.Password)) { - // if (_userManager.SupportsUserLockout) { - // await _userManager.AccessFailedAsync(user); - // } - // - // return BadRequest(new OpenIdConnectResponse { - // Error = OpenIdConnectConstants.Errors.InvalidGrant, - // ErrorDescription = "The username/password couple is invalid." - // }); - // } - // - // 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 BadRequest(new OpenIdConnectResponse { - // Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, - // ErrorDescription = "The specified grant type is not supported." - // }); - // } + // Note: to support non-interactive flows like password, + // you must provide your own token endpoint action: + + [HttpPost("~/connect/token")] + [Produces("application/json")] + public async Task Exchange() { + var request = HttpContext.GetOpenIdConnectRequest(); + + if (request.IsPasswordGrantType()) { + var user = await _userManager.FindByNameAsync(request.Username); + if (user == null) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The username/password couple is invalid." + }); + } + + // Ensure the user is allowed to sign in. + if (!await _signInManager.CanSignInAsync(user)) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The specified user is not allowed to sign in." + }); + } + + // Reject the token request if two-factor authentication has been enabled by the user. + if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user)) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The specified user is not allowed to sign in." + }); + } + + // Ensure the user is not already locked out. + if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The username/password couple is invalid." + }); + } + + // Ensure the password is valid. + if (!await _userManager.CheckPasswordAsync(user, request.Password)) { + if (_userManager.SupportsUserLockout) { + await _userManager.AccessFailedAsync(user); + } + + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The username/password couple is invalid." + }); + } + + 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 BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, + ErrorDescription = "The specified grant type is not supported." + }); + } } } \ No newline at end of file diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index f979a8d7..c87d9f59 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -38,9 +38,10 @@ namespace Mvc.Server { .EnableTokenEndpoint("/connect/token") .EnableUserinfoEndpoint("/connect/userinfo") - // Note: the Mvc.Client sample only uses the authorization code flow but you can enable - // the other flows if you need to support implicit, password or client credentials. + // Note: the Mvc.Client sample only uses the code flow and the password flow, but you + // can enable the other flows if you need to support implicit or client credentials. .AllowAuthorizationCodeFlow() + .AllowPasswordFlow() .AllowRefreshTokenFlow() // During development, you can disable the HTTPS requirement. diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index 9c7dc35c..4d830820 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -210,6 +210,8 @@ namespace OpenIddict.Infrastructure { await services.Tokens.RevokeAsync(token); context.Validate(context.Ticket); + + return; } // Note: the OpenID Connect server middleware automatically reuses the authentication ticket @@ -271,6 +273,8 @@ namespace OpenIddict.Infrastructure { context.Options.AuthenticationScheme); context.Validate(ticket); + + return; } else if (context.Request.IsPasswordGrantType()) { @@ -285,80 +289,31 @@ namespace OpenIddict.Infrastructure { "given username was not found in the database: {Username}.", context.Request.Username); } - else { - // Ensure the user is allowed to sign in. - if (!await services.SignIn.CanSignInAsync(user)) { - services.Logger.LogError("The token request was rejected because the user '{Username}' " + - "was not allowed to sign in.", context.Request.Username); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The user is not allowed to sign in."); - - return; - } - - // Ensure the user is not already locked out. - if (services.Users.SupportsUserLockout && 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.", context.Request.Username); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Account locked out."); - - return; - } + // Return an error if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + else if (services.Users.SupportsUserEmail && context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && + !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email)) { + // Retrieve the username and the email address associated with the user. + var username = await services.Users.GetUserNameAsync(user); + var email = await services.Users.GetEmailAsync(user); - // Reject the token request if two-factor authentication has been enabled by the user. - if (services.Users.SupportsUserTwoFactor && await services.Users.GetTwoFactorEnabledAsync(user)) { - services.Logger.LogError("The token request was rejected because two-factor authentication " + - "was required for the account '{Username}.", context.Request.Username); + if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { + services.Logger.LogError("The token request was rejected because the 'email' scope was not requested: " + + "to prevent data leakage, the 'email' scope must be granted when the username " + + "is identical to the email address associated with the user profile."); context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Two-factor authentication is required for this account."); + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The 'email' scope is required."); return; } - - // Return an error if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (services.Users.SupportsUserEmail && context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && - !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email)) { - // Retrieve the username and the email address associated with the user. - var username = await services.Users.GetUserNameAsync(user); - var email = await services.Users.GetEmailAsync(user); - - if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { - services.Logger.LogError("The token request was rejected because the 'email' scope was not requested: " + - "to prevent data leakage, the 'email' scope must be granted when the username " + - "is identical to the email address associated with the user profile."); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The 'email' scope is required."); - - return; - } - } } - - context.SkipToNextMiddleware(); } - else if (context.Request.IsClientCredentialsGrantType()) { - // Note: at this stage, the client credentials cannot be null or invalid, as client authentication is required - // to use the client credentials grant and is automatically enforced by the OpenID Connect server middleware. - Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId) && - !string.IsNullOrEmpty(context.Request.ClientSecret), "The client credentials shouldn't be null."); - - context.SkipToNextMiddleware(); - } - - else { - context.SkipToNextMiddleware(); - } + // Invoke the rest of the pipeline to allow + // the user code to handle the token request. + context.SkipToNextMiddleware(); } } } \ No newline at end of file