Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

501 lines
28 KiB

/*
* 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 System;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Infrastructure {
public partial class OpenIddictProvider<TUser, TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TUser : class where TApplication : class where TAuthorization : class where TScope : class where TToken : class {
public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
// Note: the OpenID Connect server middleware supports authorization code, refresh token, client credentials,
// resource owner password credentials and custom grants but OpenIddict uses a stricter policy rejecting custom grants.
if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType() &&
!context.Request.IsPasswordGrantType() && !context.Request.IsClientCredentialsGrantType()) {
services.Logger.LogError("The token request was rejected because the '{Grant}' " +
"grant is not supported.", context.Request.GrantType);
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only authorization code, refresh token, client credentials " +
"and password grants are accepted by this authorization server.");
return;
}
// Reject token requests using grant_type=authorization_code
// if the authorization code flow support is not enabled.
if (context.Request.IsAuthorizationCodeGrantType() && !services.Options.IsAuthorizationCodeFlowEnabled()) {
services.Logger.LogError("The token request was rejected because the authorization code flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "The specified grant_type parameter is not allowed.");
return;
}
// Reject token requests using grant_type=client_credentials
// if the client credentials flow support is not enabled.
else if (context.Request.IsClientCredentialsGrantType() && !services.Options.IsClientCredentialsFlowEnabled()) {
services.Logger.LogError("The token request was rejected because the client credentials flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "The specified grant_type parameter is not allowed.");
return;
}
// Reject token requests using grant_type=password if the
// resource owner password credentials flow support is not enabled.
else if (context.Request.IsPasswordGrantType() && !services.Options.IsPasswordFlowEnabled()) {
services.Logger.LogError("The token request was rejected because the resource " +
"owner password credentials flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "The specified grant_type parameter is not allowed.");
return;
}
// Reject token requests using grant_type=refresh_token
// if the refresh token flow support is not enabled.
if (context.Request.IsRefreshTokenGrantType() && !services.Options.IsRefreshTokenFlowEnabled()) {
services.Logger.LogError("The token request was rejected because the refresh token flow was not enabled.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "The specified grant_type parameter is not allowed.");
return;
}
// Reject token requests that specify scope=offline_access if the refresh token flow is not enabled.
if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && !services.Options.IsRefreshTokenFlowEnabled()) {
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'offline_access' scope is not allowed.");
return;
}
// Note: the OpenID Connect server middleware allows returning a refresh token with grant_type=client_credentials,
// though it's usually not recommended by the OAuth2 specification. To encourage developers to make a new
// grant_type=client_credentials request instead of using refresh tokens, OpenIddict uses a stricter policy
// that rejects grant_type=client_credentials requests containing the 'offline_access' scope.
// See https://tools.ietf.org/html/rfc6749#section-4.4.3 for more information.
if (context.Request.IsClientCredentialsGrantType() &&
context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess)) {
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "The 'offline_access' scope is not allowed when using grant_type=client_credentials.");
return;
}
// Note: the OpenID Connect server middleware rejects grant_type=client_credentials requests
// when validation is skipped but an early check is made here to avoid making unnecessary
// database roundtrips to retrieve the client application corresponding to the client_id.
if (context.Request.IsClientCredentialsGrantType() && (string.IsNullOrEmpty(context.Request.ClientId) ||
string.IsNullOrEmpty(context.Request.ClientSecret))) {
services.Logger.LogError("The token request was rejected because the client credentials were missing.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Client applications must be authenticated to use the client credentials grant.");
return;
}
// Note: though required by the OpenID Connect specification for the refresh token grant,
// client authentication is not mandatory for non-confidential client applications in OAuth2.
// To avoid breaking OAuth2 scenarios, OpenIddict uses a relaxed policy that allows
// public applications to use the refresh token grant without having to authenticate.
// See http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
// and https://tools.ietf.org/html/rfc6749#section-6 for more information.
// At this stage, skip client authentication if the client identifier is missing.
// Note: the OpenID Connect server middleware will automatically ensure that
// the calling application cannot use an authorization code or a refresh token
// if it's not the intended audience, even if client authentication was skipped.
if (string.IsNullOrEmpty(context.ClientId)) {
services.Logger.LogInformation("The token request validation process was skipped " +
"because the client_id parameter was missing or empty.");
context.Skip();
return;
}
// Retrieve the application details corresponding to the requested client_id.
var application = await services.Applications.FindByClientIdAsync(context.ClientId);
if (application == null) {
services.Logger.LogError("The token request was rejected because the client " +
"application was not found: '{ClientId}'.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Application not found in the database: ensure that your client_id is correct.");
return;
}
if (await services.Applications.IsPublicAsync(application)) {
// Note: public applications are not allowed to use the client credentials grant.
if (context.Request.IsClientCredentialsGrantType()) {
services.Logger.LogError("The token request was rejected because the public client application '{ClientId}' " +
"was not allowed to use the client credentials grant.", context.Request.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.UnauthorizedClient,
description: "Public clients are not allowed to use the client credentials grant.");
return;
}
// Reject tokens requests containing a client_secret when the client is a public application.
if (!string.IsNullOrEmpty(context.ClientSecret)) {
services.Logger.LogError("The token request was rejected because the public application '{ClientId}' " +
"was not allowed to send a client secret.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Public clients are not allowed to send a client_secret.");
return;
}
services.Logger.LogInformation("The token request validation process was not fully validated because " +
"the client '{ClientId}' was a public application.", context.ClientId);
// If client authentication cannot be enforced, call context.Skip() to inform
// the OpenID Connect server middleware that the caller cannot be fully trusted.
context.Skip();
return;
}
// Confidential applications MUST authenticate
// to protect them from impersonation attacks.
if (string.IsNullOrEmpty(context.ClientSecret)) {
services.Logger.LogError("The token request was rejected because the confidential application " +
"'{ClientId}' didn't specify a client secret.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Missing credentials: ensure that you specified a client_secret.");
return;
}
if (!await services.Applications.ValidateSecretAsync(application, context.ClientSecret)) {
services.Logger.LogError("The token request was rejected because the confidential application " +
"'{ClientId}' didn't specify valid client credentials.", context.ClientId);
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Invalid credentials: ensure that you specified a correct client_secret.");
return;
}
context.Validate();
}
public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
Debug.Assert(context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsClientCredentialsGrantType() ||
context.Request.IsPasswordGrantType() ||
context.Request.IsRefreshTokenGrantType(), "The grant_type parameter should be a supported value.");
// Note: the OpenID Connect server middleware automatically reuses the authentication ticket
// stored in the authorization code to create a new identity. To ensure the user was not removed
// after the authorization code was issued, a new check is made before validating the request.
if (context.Request.IsAuthorizationCodeGrantType()) {
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
var user = await services.Users.GetUserAsync(context.Ticket.Principal);
if (user == null) {
services.Logger.LogError("The token request was rejected because the user profile associated " +
"with the authorization code was not found in the database: '{Identifier}'.",
context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier));
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The authorization code is no longer valid.");
return;
}
// Extract the token identifier from the authorization code.
var identifier = context.Ticket.GetTicketId();
Debug.Assert(!string.IsNullOrEmpty(identifier),
"The authorization code should contain a ticket identifier.");
// Retrieve the token from the database and ensure it is still valid.
var token = await services.Tokens.FindByIdAsync(identifier);
if (token == null) {
services.Logger.LogError("The token request was rejected because the authorization code was revoked.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The authorization code is no longer valid.");
return;
}
// Revoke the authorization code to prevent token reuse.
await services.Tokens.RevokeAsync(token);
context.Validate(context.Ticket);
}
// Note: the OpenID Connect server middleware automatically reuses the authentication ticket
// stored in the refresh token to create a new identity. To ensure the user was not removed
// after the refresh token was issued, a new check is made before validating the request.
else if (context.Request.IsRefreshTokenGrantType()) {
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
var user = await services.Users.GetUserAsync(context.Ticket.Principal);
if (user == null) {
services.Logger.LogError("The token request was rejected because the user profile associated " +
"with the refresh token was not found in the database: '{Identifier}'.",
context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier));
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The refresh token is no longer valid.");
return;
}
// Extract the token identifier from the refresh token.
var identifier = context.Ticket.GetTicketId();
Debug.Assert(!string.IsNullOrEmpty(identifier),
"The refresh token should contain a ticket identifier.");
// Retrieve the token from the database and ensure it is still valid.
var token = await services.Tokens.FindByIdAsync(identifier);
if (token == null) {
services.Logger.LogError("The token request was rejected because the refresh token was revoked.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The refresh token is no longer valid.");
return;
}
// When sliding expiration is enabled, immediately
// revoke the refresh token to prevent future reuse.
// See https://tools.ietf.org/html/rfc6749#section-6.
if (context.Options.UseSlidingExpiration) {
await services.Tokens.RevokeAsync(token);
}
// Note: the "scopes" property stored in context.AuthenticationTicket is automatically updated by the
// OpenID Connect server middleware when the client application requests a restricted scopes collection.
var identity = await services.Users.CreateIdentityAsync(user, context.Ticket.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)}'.");
}
// Create a new authentication ticket holding the user identity but
// reuse the authentication properties stored in the refresh token.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
context.Ticket.Properties,
context.Options.AuthenticationScheme);
context.Validate(ticket);
}
else if (context.Request.IsPasswordGrantType()) {
// Note: at this stage, the client credentials cannot be null as the OpenID Connect server middleware
// automatically rejects grant_type=password requests that don't specify a username/password couple.
Debug.Assert(!string.IsNullOrEmpty(context.Request.Username) &&
!string.IsNullOrEmpty(context.Request.Password), "The user credentials shouldn't be null.");
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;
}
// 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;
}
// 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: "Account locked out.");
}
}
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.");
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;
}
}
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)}'.");
}
// Create a new authentication ticket holding the user 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);
}
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.");
// 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);
}
}
}
}