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;
}
// 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<IActionResult> Authorize() {
// Extract the authorization request from the ASP.NET environment.
@ -97,6 +100,9 @@ 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.

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);
if (user == null) {
services.Logger.LogError("The token request was rejected because no user profile corresponding to " +
"the specified username was found: '{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;
services.Logger.LogWarning("The token request was not fully validated because the profile corresponding to the " +
"given username was not found in the database: {Username}.", context.Request.Username);
}
// 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;
}
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);
// Ensure the password is valid.
if (!await services.Users.CheckPasswordAsync(user, context.Request.Password)) {
services.Logger.LogError("The token request was rejected because the password didn't match " +
"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(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The user is not allowed to sign in.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Account locked out.");
}
return;
}
return;
}
if (services.Users.SupportsUserLockout) {
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.");
// 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);
return;
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Account locked out.");
// 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);
return;
}
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.");
// 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.InvalidRequest,
description: "The 'email' scope is required.");
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Two-factor authentication is required for this account.");
return;
}
}
var identity = await services.Users.CreateIdentityAsync(user, context.Request.GetScopes());
if (identity == null) {
throw new InvalidOperationException("The token request was aborted because the user manager returned a null " +
$"identity for user '{await services.Users.GetUserNameAsync(user)}'.");
}
// 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);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
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.");
ticket.SetResources(context.Request.GetResources());
ticket.SetScopes(context.Request.GetScopes());
context.Reject(
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()) {
@ -459,42 +414,11 @@ namespace OpenIddict.Infrastructure {
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId) &&
!string.IsNullOrEmpty(context.Request.ClientSecret), "The client credentials shouldn't be null.");
// 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(context.Request.ClientId);
if (application == null) {
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);
// 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();
}
}
}

24
src/OpenIddict.Core/OpenIddictExtensions.cs

@ -7,6 +7,7 @@
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;
@ -63,6 +64,16 @@ namespace Microsoft.AspNetCore.Builder {
builder.Configure(options => {
// Register the OpenID Connect server provider in the OpenIddict options.
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.
@ -73,6 +84,12 @@ namespace Microsoft.AspNetCore.Builder {
builder.Services.TryAddScoped<OpenIddictUserManager<TUser>>();
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;
}
@ -132,13 +149,8 @@ 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 modules.OrderBy(module => module.Position)) {
foreach (var module in options.Modules.OrderBy(module => module.Position)) {
if (module?.Registration == null) {
throw new InvalidOperationException("An invalid OpenIddict module was registered.");
}

Loading…
Cancel
Save