Browse Source

Allow handling grant_type=password and grant_type=client_credentials requests in user code and introduce OpenIddictMiddleware

pull/190/head
Kévin Chalet 10 years ago
parent
commit
1db3779ab3
  1. 6
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  2. 180
      src/OpenIddict.Core/Infrastructure/OpenIddictMiddleware.cs
  3. 186
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs
  4. 24
      src/OpenIddict.Core/OpenIddictExtensions.cs

6
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -36,6 +36,9 @@ namespace Mvc.Server {
_userManager = userManager; _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")] [Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize() { public async Task<IActionResult> Authorize() {
// Extract the authorization request from the ASP.NET environment. // Extract the authorization request from the ASP.NET environment.
@ -97,6 +100,9 @@ namespace Mvc.Server {
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme); 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")] [HttpGet("~/connect/logout")]
public IActionResult Logout() { public IActionResult Logout() {
// Extract the authorization request from the ASP.NET environment. // Extract the authorization request from the ASP.NET environment.

180
src/OpenIddict.Core/Infrastructure/OpenIddictMiddleware.cs

@ -0,0 +1,180 @@
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<TUser, TApplication, TAuthorization, TScope, TToken>
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<OpenIddictServices<
TUser, TApplication, TAuthorization, TScope, TToken>>();
// 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);
}
}
}
}

186
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs

@ -338,119 +338,74 @@ namespace OpenIddict.Infrastructure {
var user = await services.Users.FindByNameAsync(context.Request.Username); var user = await services.Users.FindByNameAsync(context.Request.Username);
if (user == null) { if (user == null) {
services.Logger.LogError("The token request was rejected because no user profile corresponding to " + services.Logger.LogWarning("The token request was not fully validated because the profile corresponding to the " +
"the specified username was found: '{Username}'.", context.Request.Username); "given username was not found in the database: {Username}.", context.Request.Username);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
// 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. else {
if (services.Users.SupportsUserLockout && await services.Users.IsLockedOutAsync(user)) { // Ensure the user is allowed to sign in.
services.Logger.LogError("The token request was rejected because the account '{Username}' " + if (!await services.SignIn.CanSignInAsync(user)) {
"was locked out to prevent brute force attacks.", context.Request.Username); 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: "Account locked out.");
return;
}
// Ensure the password is valid. context.Reject(
if (!await services.Users.CheckPasswordAsync(user, context.Request.Password)) { error: OpenIdConnectConstants.Errors.InvalidGrant,
services.Logger.LogError("The token request was rejected because the password didn't match " + description: "The user is not allowed to sign in.");
"the password associated with the account '{Username}'.", context.Request.Username);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
if (services.Users.SupportsUserLockout) {
await services.Users.AccessFailedAsync(user);
// Ensure the user is not locked out.
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.", context.Request.Username);
context.Reject( return;
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Account locked out.");
}
} }
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}' " +
if (services.Users.SupportsUserLockout) { "was locked out to prevent brute force attacks.", context.Request.Username);
await services.Users.ResetAccessFailedCountAsync(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);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Two-factor authentication is required for this account.");
return; context.Reject(
} error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Account locked out.");
// Return an error if the username corresponds to the registered return;
// 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)) { // Reject the token request if two-factor authentication has been enabled by the user.
services.Logger.LogError("The token request was rejected because the 'email' scope was not requested: " + if (services.Users.SupportsUserTwoFactor && await services.Users.GetTwoFactorEnabledAsync(user)) {
"to prevent data leakage, the 'email' scope must be granted when the username " + services.Logger.LogError("The token request was rejected because two-factor authentication " +
"is identical to the email address associated with the user profile."); "was required for the account '{Username}.", context.Request.Username);
context.Reject( context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest, error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The 'email' scope is required."); description: "Two-factor authentication is required for this account.");
return; return;
} }
}
var identity = await services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); // Return an error if the username corresponds to the registered
if (identity == null) { // email address and if the "email" scope has not been requested.
throw new InvalidOperationException("The token request was aborted because the user manager returned a null " + if (services.Users.SupportsUserEmail && context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) &&
$"identity for user '{await services.Users.GetUserNameAsync(user)}'."); !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);
// Create a new authentication ticket holding the user identity. if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) {
var ticket = new AuthenticationTicket( services.Logger.LogError("The token request was rejected because the 'email' scope was not requested: " +
new ClaimsPrincipal(identity), "to prevent data leakage, the 'email' scope must be granted when the username " +
new AuthenticationProperties(), "is identical to the email address associated with the user profile.");
context.Options.AuthenticationScheme);
ticket.SetResources(context.Request.GetResources()); context.Reject(
ticket.SetScopes(context.Request.GetScopes()); error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'email' scope is required.");
context.Validate(ticket); return;
}
}
}
// 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();
} }
else if (context.Request.IsClientCredentialsGrantType()) { else if (context.Request.IsClientCredentialsGrantType()) {
@ -459,42 +414,11 @@ namespace OpenIddict.Infrastructure {
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId) && Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId) &&
!string.IsNullOrEmpty(context.Request.ClientSecret), "The client credentials shouldn't be null."); !string.IsNullOrEmpty(context.Request.ClientSecret), "The client credentials shouldn't be null.");
// Retrieve the application details corresponding to the requested client_id. // Call context.SkipToNextMiddleware() to invoke the next middleware in the pipeline.
// Note: this call shouldn't return a null instance, but a race condition may occur // This allows handling grant_type=client_credentials requests in a custom controller action.
// if the application was removed after the initial check made by ValidateTokenRequest. // If the request is not handled in user code, OpenIddictMiddleware will automatically
var application = await services.Applications.FindByClientIdAsync(context.Request.ClientId); // create and return a token response using the default authentication logic.
if (application == null) { context.SkipToNextMiddleware();
services.Logger.LogError("The token request was aborted because the client application " +
"was not found in the database: '{ClientId}'.", context.Request.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Application not found in the database: ensure that your client_id is correct.");
return;
}
var identity = new ClaimsIdentity(context.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, context.Request.ClientId);
identity.AddClaim(ClaimTypes.Name, await services.Applications.GetDisplayNameAsync(application),
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// Create a new authentication ticket
// holding the application identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
ticket.SetResources(context.Request.GetResources());
ticket.SetScopes(context.Request.GetScopes());
context.Validate(ticket);
} }
} }
} }

24
src/OpenIddict.Core/OpenIddictExtensions.cs

@ -7,6 +7,7 @@
using System; using System;
using System.Linq; using System.Linq;
using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -63,6 +64,16 @@ namespace Microsoft.AspNetCore.Builder {
builder.Configure(options => { builder.Configure(options => {
// Register the OpenID Connect server provider in the OpenIddict options. // Register the OpenID Connect server provider in the OpenIddict options.
options.Provider = new OpenIddictProvider<TUser, TApplication, TAuthorization, TScope, TToken>(); options.Provider = new OpenIddictProvider<TUser, TApplication, TAuthorization, TScope, TToken>();
// Register the OpenID Connect server middleware as a native module.
options.Modules.Add(new OpenIddictModule("OpenID Connect server", 0, app => {
app.UseMiddleware<OpenIdConnectServerMiddleware>();
}));
// Register the OpenIddict middleware as a built-in module.
options.Modules.Add(new OpenIddictModule("OpenIddict", 1, app => {
app.UseMiddleware<OpenIddictMiddleware<TUser, TApplication, TAuthorization, TScope, TToken>>();
}));
}); });
// Register the OpenIddict core services in the DI container. // Register the OpenIddict core services in the DI container.
@ -73,6 +84,12 @@ namespace Microsoft.AspNetCore.Builder {
builder.Services.TryAddScoped<OpenIddictUserManager<TUser>>(); builder.Services.TryAddScoped<OpenIddictUserManager<TUser>>();
builder.Services.TryAddScoped<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>(); builder.Services.TryAddScoped<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
// Override the default options manager for IOptions<OpenIdConnectServerOptions>
// to ensure the OpenID Connect server middleware uses the OpenIddict options.
builder.Services.TryAddScoped<IOptions<OpenIdConnectServerOptions>>(provider => {
return provider.GetRequiredService<IOptions<OpenIddictOptions>>();
});
return builder; return builder;
} }
@ -132,13 +149,8 @@ namespace Microsoft.AspNetCore.Builder {
"client credentials, password and refresh token flows."); "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. // Register the OpenIddict modules in the ASP.NET Core pipeline.
foreach (var module in modules.OrderBy(module => module.Position)) { foreach (var module in options.Modules.OrderBy(module => module.Position)) {
if (module?.Registration == null) { if (module?.Registration == null) {
throw new InvalidOperationException("An invalid OpenIddict module was registered."); throw new InvalidOperationException("An invalid OpenIddict module was registered.");
} }

Loading…
Cancel
Save