diff --git a/src/OpenIddict.Core/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/OpenIddictProvider.Authentication.cs new file mode 100644 index 00000000..cb173f97 --- /dev/null +++ b/src/OpenIddict.Core/OpenIddictProvider.Authentication.cs @@ -0,0 +1,172 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNet.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; + +namespace OpenIddict { + public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { + public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + // Note: redirect_uri is not required for pure OAuth2 requests + // but this provider uses a stricter policy making it mandatory, + // as required by the OpenID Connect core specification. + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. + if (string.IsNullOrEmpty(context.RedirectUri)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The required redirect_uri parameter was missing."); + + return; + } + + // Retrieve the application details corresponding to the requested client_id. + var application = await manager.FindApplicationByIdAsync(context.ClientId); + if (application == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Application not found in the database: ensure that your client_id is correct."); + + return; + } + + if (!await manager.ValidateRedirectUriAsync(application, context.RedirectUri)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Invalid redirect_uri."); + + return; + } + + // To prevent downgrade attacks, ensure that authorization requests using the hybrid/implicit + // flow are rejected if the client identifier corresponds to a confidential application. + // Note: when using the authorization code grant, ValidateClientAuthentication is responsible of + // rejecting the token request if the client_id corresponds to an unauthenticated confidential client. + if (await manager.IsConfidentialApplicationAsync(application) && !context.Request.IsAuthorizationCodeFlow()) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "Confidential clients can only use response_type=code."); + + return; + } + + // Run additional checks for prompt=none requests. + if (string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { + // If the user is not authenticated, return an error to the client application. + // See http://openid.net/specs/openid-connect-core-1_0.html#Authenticates + if (!context.HttpContext.User.Identities.Any(identity => identity.IsAuthenticated)) { + context.Reject( + error: OpenIdConnectConstants.Errors.LoginRequired, + description: "The user must be authenticated."); + + return; + } + + // Extract the principal contained in the id_token_hint parameter. + // If no principal can be extracted, an error is returned to the client application. + var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); + if (principal == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The required id_token_hint parameter is missing."); + + return; + } + + // Ensure the client application is listed as a valid audience in the identity token + // and that the identity token corresponds to the authenticated user. + if (!principal.HasClaim(JwtRegisteredClaimNames.Aud, context.Request.ClientId) || + !principal.HasClaim(ClaimTypes.NameIdentifier, context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier))) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The id_token_hint parameter is invalid."); + + return; + } + + // Ensure the user profile still exists in the database. + var user = await manager.FindByIdAsync(principal.GetUserId()); + if (user == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The id_token_hint parameter is invalid."); + + return; + } + + // Return an error if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && + !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email) && + string.Equals(await manager.GetUserNameAsync(user), + await manager.GetEmailAsync(user), + StringComparison.OrdinalIgnoreCase)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The 'email' scope is required."); + + return; + } + } + + context.Validate(); + } + + public override async Task AuthorizationEndpoint([NotNull] AuthorizationEndpointContext context) { + // Only handle prompt=none requests at this stage. + if (!string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { + return; + } + + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + // Note: principal is guaranteed to be non-null since ValidateAuthorizationRequest + // rejects prompt=none requests missing or having an invalid id_token_hint. + var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); + Debug.Assert(principal != null); + + // Note: user may be null if the user was removed after + // the initial check made by ValidateAuthorizationRequest. + // In this case, ignore the prompt=none request and + // continue to the next middleware in the pipeline. + var user = await manager.FindByIdAsync(principal.GetUserId()); + if (user == null) { + return; + } + + // Note: filtering the username is not needed at this stage as OpenIddictController.Accept + // and OpenIddictProvider.GrantResourceOwnerCredentials are expected to reject requests that + // don't include the "email" scope if the username corresponds to the registed email address. + var identity = await manager.CreateIdentityAsync(user, context.Request.GetScopes()); + Debug.Assert(identity != null); + + // Create a new authentication ticket holding the user identity. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + null, context.Options.AuthenticationScheme); + + ticket.SetResources(context.Request.GetResources()); + ticket.SetScopes(context.Request.GetScopes()); + + // Call SignInAsync to create and return a new OpenID Connect response containing the serialized code/tokens. + await context.HttpContext.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); + + // Mark the response as handled + // to skip the rest of the pipeline. + context.HandleResponse(); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/OpenIddictProvider.Exchange.cs new file mode 100644 index 00000000..16100cac --- /dev/null +++ b/src/OpenIddict.Core/OpenIddictProvider.Exchange.cs @@ -0,0 +1,225 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/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 Microsoft.AspNet.Authentication; +using Microsoft.AspNet.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; + +namespace OpenIddict { + public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { + public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + // Note: OpenIdConnectServerHandler supports authorization code, refresh token, + // client credentials, resource owner password credentials and custom grants + // but this authorization server uses a stricter policy rejecting custom grant types. + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType() && + !context.Request.IsPasswordGrantType() && !context.Request.IsClientCredentialsGrantType()) { + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedGrantType, + description: "Only authorization code, refresh token, client credentials " + + "and password grants are accepted by this authorization server."); + + 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. + + // Skip client authentication if the client identifier is missing. + // Note: ASOS 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)) { + context.Skip(); + + return; + } + + // Retrieve the application details corresponding to the requested client_id. + var application = await manager.FindApplicationByIdAsync(context.ClientId); + if (application == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Application not found in the database: ensure that your client_id is correct."); + + return; + } + + // Reject tokens requests containing a client_secret if the client application is not confidential. + if (await manager.IsPublicApplicationAsync(application) && !string.IsNullOrEmpty(context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "Public clients are not allowed to send a client_secret."); + + return; + } + + // Confidential applications MUST authenticate + // to protect them from impersonation attacks. + else if (await manager.IsConfidentialApplicationAsync(application)) { + if (string.IsNullOrEmpty(context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Missing credentials: ensure that you specified a client_secret."); + + return; + } + + if (!await manager.ValidateSecretAsync(application, context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Invalid credentials: ensure that you specified a correct client_secret."); + + return; + } + } + + context.Validate(); + } + + public override async Task GrantClientCredentials([NotNull] GrantClientCredentialsContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + // Retrieve the application details corresponding to the requested client_id. + var application = await manager.FindApplicationByIdAsync(context.ClientId); + Debug.Assert(application != null); + + var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, context.ClientId, destination: "id_token token"); + identity.AddClaim(ClaimTypes.Name, await manager.GetDisplayNameAsync(application), destination: "id_token token"); + + // Create a new authentication ticket + // holding the application identity. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + null, context.Options.AuthenticationScheme); + + ticket.SetResources(context.Request.GetResources()); + ticket.SetScopes(context.Request.GetScopes()); + + context.Validate(ticket); + } + + public override async Task GrantRefreshToken([NotNull] GrantRefreshTokenContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + var options = context.HttpContext.RequestServices.GetRequiredService>(); + + // If the user manager doesn't support security + // stamps, skip the default validation logic. + if (!manager.SupportsUserSecurityStamp) { + return; + } + + var principal = context.AuthenticationTicket?.Principal; + Debug.Assert(principal != null); + + var user = await manager.FindByIdAsync(principal.GetUserId()); + if (user == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); + + return; + } + + var identifier = principal.GetClaim(options.Value.ClaimsIdentity.SecurityStampClaimType); + if (!string.IsNullOrEmpty(identifier) && + !string.Equals(identifier, await manager.GetSecurityStampAsync(user), StringComparison.Ordinal)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); + + return; + } + } + + public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + var user = await manager.FindByNameAsync(context.UserName); + if (user == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Invalid credentials."); + + return; + } + + // Ensure the user is not already locked out. + if (manager.SupportsUserLockout && await manager.IsLockedOutAsync(user)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Account locked out."); + + return; + } + + // Ensure the password is valid. + if (!await manager.CheckPasswordAsync(user, context.Password)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Invalid credentials."); + + if (manager.SupportsUserLockout) { + await manager.AccessFailedAsync(user); + + // Ensure the user is not locked out. + if (await manager.IsLockedOutAsync(user)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Account locked out."); + } + } + + return; + } + + if (manager.SupportsUserLockout) { + await manager.ResetAccessFailedCountAsync(user); + } + + // Return an error if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && + !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email) && + string.Equals(await manager.GetUserNameAsync(user), + await manager.GetEmailAsync(user), + StringComparison.OrdinalIgnoreCase)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The 'email' scope is required."); + + return; + } + + var identity = await manager.CreateIdentityAsync(user, context.Request.GetScopes()); + Debug.Assert(identity != null); + + // Create a new authentication ticket holding the user identity. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + null, context.Options.AuthenticationScheme); + + ticket.SetResources(context.Request.GetResources()); + ticket.SetScopes(context.Request.GetScopes()); + + context.Validate(ticket); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictProvider.Introspection.cs b/src/OpenIddict.Core/OpenIddictProvider.Introspection.cs new file mode 100644 index 00000000..5d75957b --- /dev/null +++ b/src/OpenIddict.Core/OpenIddictProvider.Introspection.cs @@ -0,0 +1,104 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/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 Microsoft.AspNet.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; + +namespace OpenIddict { + public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { + public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + // Note: ASOS supports both GET and POST introspection requests but OpenIddict only accepts POST requests. + if (!string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "Introspection requests must use HTTP POST."); + + return; + } + + // Note: ASOS supports unauthenticated introspection requests but OpenIddict uses + // a stricter policy preventing unauthenticated/public applications from using + // the introspection endpoint, as required by the specifications. + // See https://tools.ietf.org/html/rfc7662 for more information. + if (string.IsNullOrEmpty(context.ClientId) || string.IsNullOrEmpty(context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "Clients must be authenticated to use the introspection endpoint."); + + return; + } + + // Retrieve the application details corresponding to the requested client_id. + var application = await manager.FindApplicationByIdAsync(context.ClientId); + if (application == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Application not found in the database: ensure that your client_id is correct."); + + return; + } + + // Reject non-confidential applications. + if (await manager.IsPublicApplicationAsync(application)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Public applications are not allowed to use the introspection endpoint."); + + return; + } + + // Validate the client credentials. + if (!await manager.ValidateSecretAsync(application, context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Invalid credentials: ensure that you specified a correct client_secret."); + + return; + } + + context.Validate(); + } + + public override async Task IntrospectionEndpoint([NotNull] IntrospectionEndpointContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + var options = context.HttpContext.RequestServices.GetRequiredService>(); + + // If the user manager doesn't support security + // stamps, skip the additional validation logic. + if (!manager.SupportsUserSecurityStamp) { + return; + } + + var principal = context.AuthenticationTicket?.Principal; + Debug.Assert(principal != null); + + var user = await manager.FindByIdAsync(principal.GetUserId()); + if (user == null) { + context.Active = false; + + return; + } + + var identifier = principal.GetClaim(options.Value.ClaimsIdentity.SecurityStampClaimType); + if (!string.IsNullOrEmpty(identifier) && + !string.Equals(identifier, await manager.GetSecurityStampAsync(user), StringComparison.Ordinal)) { + context.Active = false; + + return; + } + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictProvider.Session.cs b/src/OpenIddict.Core/OpenIddictProvider.Session.cs new file mode 100644 index 00000000..9d47f45b --- /dev/null +++ b/src/OpenIddict.Core/OpenIddictProvider.Session.cs @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; + +namespace OpenIddict { + public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { + public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) { + var manager = context.HttpContext.RequestServices.GetRequiredService>(); + + // Skip validation if the optional post_logout_redirect_uri + // parameter was missing from the logout request. + if (string.IsNullOrEmpty(context.PostLogoutRedirectUri)) { + context.Skip(); + + return; + } + + var application = await manager.FindApplicationByLogoutRedirectUri(context.PostLogoutRedirectUri); + if (application == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Invalid post_logout_redirect_uri."); + + return; + } + + context.Validate(); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictProvider.cs b/src/OpenIddict.Core/OpenIddictProvider.cs index 5a06cb5c..bcbfc5f7 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.cs +++ b/src/OpenIddict.Core/OpenIddictProvider.cs @@ -4,22 +4,16 @@ * the license and the contributors participating to this project. */ -using System; using System.Diagnostics; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; -using Microsoft.AspNet.Authentication; -using Microsoft.AspNet.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Options; namespace OpenIddict { - public class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { + public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { public override Task MatchEndpoint([NotNull] MatchEndpointContext context) { // Note: by default, OpenIdConnectServerHandler only handles authorization requests made to AuthorizationEndpointPath. // This context handler uses a more relaxed policy that allows extracting authorization requests received at @@ -32,261 +26,6 @@ namespace OpenIddict { return Task.FromResult(null); } - public override async Task ValidateClientRedirectUri([NotNull] ValidateClientRedirectUriContext context) { - // Note: redirect_uri is not required for pure OAuth2 requests but this provider uses a stricter policy making it mandatory, - // as required by the OpenID Connect specification: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest - if (string.IsNullOrEmpty(context.RedirectUri)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The required redirect_uri parameter was missing."); - - return; - } - - // Retrieve the application details corresponding to the requested client_id. - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - var application = await manager.FindApplicationByIdAsync(context.ClientId); - if (application == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Application not found in the database: ensure that your client_id is correct."); - - return; - } - - if (!await manager.ValidateRedirectUriAsync(application, context.RedirectUri)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Invalid redirect_uri."); - - return; - } - - context.Validate(); - } - - public override async Task ValidateClientLogoutRedirectUri([NotNull] ValidateClientLogoutRedirectUriContext context) { - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - var application = await manager.FindApplicationByLogoutRedirectUri(context.PostLogoutRedirectUri); - if (application == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Invalid post_logout_redirect_uri."); - - return; - } - - context.Validate(); - } - - public override async Task ValidateClientAuthentication([NotNull] ValidateClientAuthenticationContext context) { - // 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. - - // Skip client authentication if the client identifier is missing. - // Note: ASOS 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)) { - context.Skip(); - - return; - } - - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - // Retrieve the application details corresponding to the requested client_id. - var application = await manager.FindApplicationByIdAsync(context.ClientId); - if (application == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Application not found in the database: ensure that your client_id is correct."); - - return; - } - - // Reject tokens requests containing a client_secret if the client application is not confidential. - if (await manager.IsPublicApplicationAsync(application) && !string.IsNullOrEmpty(context.ClientSecret)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "Public clients are not allowed to send a client_secret."); - - return; - } - - // Confidential applications MUST authenticate - // to protect them from impersonation attacks. - else if (await manager.IsConfidentialApplicationAsync(application)) { - if (string.IsNullOrEmpty(context.ClientSecret)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Missing credentials: ensure that you specified a client_secret."); - - return; - } - - if (!await manager.ValidateSecretAsync(application, context.ClientSecret)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Invalid credentials: ensure that you specified a correct client_secret."); - - return; - } - } - - context.Validate(); - } - - public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) { - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - // Retrieve the application details corresponding to the requested client_id. - var application = await manager.FindApplicationByIdAsync(context.ClientId); - Debug.Assert(application != null); - - // To prevent downgrade attacks, ensure that authorization requests using the hybrid/implicit - // flow are rejected if the client identifier corresponds to a confidential application. - // Note: when using the authorization code grant, ValidateClientAuthentication is responsible of - // rejecting the token request if the client_id corresponds to an unauthenticated confidential client. - if (await manager.IsConfidentialApplicationAsync(application) && !context.Request.IsAuthorizationCodeFlow()) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "Confidential clients can only use response_type=code."); - - return; - } - - if (string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { - // If the user is not authenticated, return an error to the client application. - // See http://openid.net/specs/openid-connect-core-1_0.html#Authenticates - if (!context.HttpContext.User.Identities.Any(identity => identity.IsAuthenticated)) { - context.Reject( - error: OpenIdConnectConstants.Errors.LoginRequired, - description: "The user must be authenticated."); - - return; - } - - // Extract the principal contained in the id_token_hint parameter. - // If no principal can be extracted, an error is returned to the client application. - var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); - if (principal == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The required id_token_hint parameter is missing."); - - return; - } - - // Ensure the client application is listed as a valid audience in the identity token. - if (!principal.HasClaim(JwtRegisteredClaimNames.Aud, context.Request.ClientId)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The id_token_hint parameter is invalid."); - - return; - } - - // Ensure the identity token corresponds to the authenticated user. - if (!principal.HasClaim(ClaimTypes.NameIdentifier, context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier))) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The id_token_hint parameter is invalid."); - - return; - } - - // Ensure the user profile still exists in the database. - var user = await manager.FindByIdAsync(principal.GetUserId()); - if (user == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The id_token_hint parameter is invalid."); - - return; - } - - // Return an error if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && - !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email) && - string.Equals(await manager.GetUserNameAsync(user), - await manager.GetEmailAsync(user), - StringComparison.OrdinalIgnoreCase)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The 'email' scope is required."); - - return; - } - } - } - - public override Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) { - // Note: OpenIdConnectServerHandler supports authorization code, refresh token, - // client credentials, resource owner password credentials and custom grants - // but this authorization server uses a stricter policy rejecting custom grant types. - if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType() && - !context.Request.IsPasswordGrantType() && !context.Request.IsClientCredentialsGrantType()) { - context.Reject( - error: OpenIdConnectConstants.Errors.UnsupportedGrantType, - description: "Only authorization code, refresh token, client credentials " + - "and password grants are accepted by this authorization server."); - } - - return Task.FromResult(null); - } - - public override async Task AuthorizationEndpoint([NotNull] AuthorizationEndpointContext context) { - // Only handle prompt=none requests at this stage. - if (!string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { - return; - } - - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - // Note: principal is guaranteed to be non-null since ValidateAuthorizationRequest - // rejects prompt=none requests missing or having an invalid id_token_hint. - var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); - Debug.Assert(principal != null); - - // Note: user may be null if the user was removed after - // the initial check made by ValidateAuthorizationRequest. - // In this case, ignore the prompt=none request and - // continue to the next middleware in the pipeline. - var user = await manager.FindByIdAsync(principal.GetUserId()); - if (user == null) { - return; - } - - // Note: filtering the username is not needed at this stage as OpenIddictController.Accept - // and OpenIddictProvider.GrantResourceOwnerCredentials are expected to reject requests that - // don't include the "email" scope if the username corresponds to the registed email address. - var identity = await manager.CreateIdentityAsync(user, context.Request.GetScopes()); - Debug.Assert(identity != null); - - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - null, context.Options.AuthenticationScheme); - - ticket.SetResources(context.Request.GetResources()); - ticket.SetScopes(context.Request.GetScopes()); - - // Call SignInAsync to create and return a new OpenID Connect response containing the serialized code/tokens. - await context.HttpContext.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); - - // Mark the response as handled - // to skip the rest of the pipeline. - context.HandleResponse(); - } - public override async Task ProfileEndpoint([NotNull] ProfileEndpointContext context) { var manager = context.HttpContext.RequestServices.GetRequiredService>(); @@ -343,163 +82,5 @@ namespace OpenIddict { } } } - - public override async Task ValidationEndpoint([NotNull] ValidationEndpointContext context) { - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); - - // If the user manager doesn't support security - // stamps, skip the additional validation logic. - if (!manager.SupportsUserSecurityStamp) { - return; - } - - var principal = context.AuthenticationTicket?.Principal; - Debug.Assert(principal != null); - - var user = await manager.FindByIdAsync(principal.GetUserId()); - if (user == null) { - context.Active = false; - - return; - } - - var identifier = principal.GetClaim(options.Value.ClaimsIdentity.SecurityStampClaimType); - if (!string.IsNullOrEmpty(identifier) && - !string.Equals(identifier, await manager.GetSecurityStampAsync(user), StringComparison.Ordinal)) { - context.Active = false; - - return; - } - } - - public override async Task GrantClientCredentials([NotNull] GrantClientCredentialsContext context) { - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - // Retrieve the application details corresponding to the requested client_id. - var application = await manager.FindApplicationByIdAsync(context.ClientId); - Debug.Assert(application != null); - - var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); - identity.AddClaim(ClaimTypes.NameIdentifier, context.ClientId, destination: "id_token token"); - identity.AddClaim(ClaimTypes.Name, await manager.GetDisplayNameAsync(application), destination: "id_token token"); - - // Create a new authentication ticket - // holding the application identity. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - null, context.Options.AuthenticationScheme); - - ticket.SetResources(context.Request.GetResources()); - ticket.SetScopes(context.Request.GetScopes()); - - context.Validate(ticket); - } - - public override async Task GrantRefreshToken([NotNull] GrantRefreshTokenContext context) { - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); - - // If the user manager doesn't support security - // stamps, skip the default validation logic. - if (!manager.SupportsUserSecurityStamp) { - return; - } - - var principal = context.AuthenticationTicket?.Principal; - Debug.Assert(principal != null); - - var user = await manager.FindByIdAsync(principal.GetUserId()); - if (user == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); - - return; - } - - var identifier = principal.GetClaim(options.Value.ClaimsIdentity.SecurityStampClaimType); - if (!string.IsNullOrEmpty(identifier) && - !string.Equals(identifier, await manager.GetSecurityStampAsync(user), StringComparison.Ordinal)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); - - return; - } - } - - public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) { - var manager = context.HttpContext.RequestServices.GetRequiredService>(); - - var user = await manager.FindByNameAsync(context.UserName); - if (user == null) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Invalid credentials."); - - return; - } - - // Ensure the user is not already locked out. - if (manager.SupportsUserLockout && await manager.IsLockedOutAsync(user)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Account locked out."); - - return; - } - - // Ensure the password is valid. - if (!await manager.CheckPasswordAsync(user, context.Password)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Invalid credentials."); - - if (manager.SupportsUserLockout) { - await manager.AccessFailedAsync(user); - - // Ensure the user is not locked out. - if (await manager.IsLockedOutAsync(user)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Account locked out."); - } - } - - return; - } - - if (manager.SupportsUserLockout) { - await manager.ResetAccessFailedCountAsync(user); - } - - // Return an error if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && - !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email) && - string.Equals(await manager.GetUserNameAsync(user), - await manager.GetEmailAsync(user), - StringComparison.OrdinalIgnoreCase)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The 'email' scope is required."); - - return; - } - - var identity = await manager.CreateIdentityAsync(user, context.Request.GetScopes()); - Debug.Assert(identity != null); - - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - null, context.Options.AuthenticationScheme); - - ticket.SetResources(context.Request.GetResources()); - ticket.SetScopes(context.Request.GetScopes()); - - context.Validate(ticket); - } } } \ No newline at end of file