diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index e94fc879..365e5f34 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -15,6 +15,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider @@ -37,6 +38,9 @@ namespace OpenIddict.Infrastructure { // Retrieve the application details corresponding to the requested client_id. var application = await services.Applications.FindByIdAsync(context.ClientId); if (application == null) { + services.Logger.LogError("The authorization 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."); @@ -45,6 +49,9 @@ namespace OpenIddict.Infrastructure { } if (!await services.Applications.ValidateRedirectUriAsync(application, context.RedirectUri)) { + services.Logger.LogError("The authorization request was rejected because the redirect_uri " + + "was invalid: '{RedirectUri}'.", context.RedirectUri); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, description: "Invalid redirect_uri."); @@ -72,6 +79,10 @@ namespace OpenIddict.Infrastructure { // Ensure the user profile still exists in the database. var user = await services.Users.GetUserAsync(context.HttpContext.User); if (user == null) { + services.Logger.LogError("The authorization request was rejected because the profile corresponding " + + "to the logged in user was not found in the database: {Identifier}.", + context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier)); + context.Reject( error: OpenIdConnectConstants.Errors.ServerError, description: "An internal error has occurred."); @@ -88,6 +99,10 @@ namespace OpenIddict.Infrastructure { var email = await services.Users.GetEmailAsync(user); if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { + services.Logger.LogError("The authorization 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 ({Username}).", username); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "The 'email' scope is required."); @@ -102,6 +117,8 @@ namespace OpenIddict.Infrastructure { // 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)) { + services.Logger.LogError("The prompt=none authorization request was rejected because the user was not logged in."); + context.Reject( error: OpenIdConnectConstants.Errors.LoginRequired, description: "The user must be authenticated."); @@ -112,6 +129,9 @@ namespace OpenIddict.Infrastructure { // Ensure that the authentication cookie contains the required NameIdentifier claim. var identifier = context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(identifier)) { + services.Logger.LogError("The prompt=none authorization request was rejected because the user session " + + "was invalid and didn't contain the mandatory ClaimTypes.NameIdentifier claim."); + context.Reject( error: OpenIdConnectConstants.Errors.ServerError, description: "The authorization request cannot be processed."); @@ -134,6 +154,8 @@ namespace OpenIddict.Infrastructure { // and that the identity token corresponds to the authenticated user. if (!principal.HasClaim(OpenIdConnectConstants.Claims.Audience, context.Request.ClientId) || !principal.HasClaim(ClaimTypes.NameIdentifier, identifier)) { + services.Logger.LogError("The prompt=none authorization request was rejected because the id_token_hint was invalid."); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "The id_token_hint parameter is invalid."); @@ -148,46 +170,50 @@ namespace OpenIddict.Infrastructure { public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - // Only handle prompt=none requests at this stage. - if (!string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { - return; - } + if (string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { + // 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, "The principal extracted from the id_token_hint shouldn't be null."); - // 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 services.Users.GetUserAsync(principal); - if (user == null) { - return; - } + // Note: user may be null if the user was removed after + // the initial check made by ValidateAuthorizationRequest. + // In this case, throw an exception to abort the request. + var user = await services.Users.GetUserAsync(principal); + if (user == null) { + throw new InvalidOperationException("The prompt=none authorization request was aborted because the profile " + + "corresponding to the logged in user was not found in the database."); + } - // 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 services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); - Debug.Assert(identity != null); + // 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 services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); + if (identity == null) { + throw new InvalidOperationException("The authorization request failed 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()); - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - context.Options.AuthenticationScheme); + // 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); - ticket.SetResources(context.Request.GetResources()); - ticket.SetScopes(context.Request.GetScopes()); + // Mark the response as handled + // to skip the rest of the pipeline. + context.HandleResponse(); - // 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); + return; + } - // Mark the response as handled - // to skip the rest of the pipeline. - context.HandleResponse(); + context.SkipToNextMiddleware(); } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index 11a5bf45..573e1e34 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -14,6 +14,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider @@ -25,6 +26,9 @@ namespace OpenIddict.Infrastructure { // 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 " + @@ -59,6 +63,9 @@ namespace OpenIddict.Infrastructure { // 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; @@ -67,6 +74,9 @@ namespace OpenIddict.Infrastructure { // Retrieve the application details corresponding to the requested client_id. var application = await services.Applications.FindByIdAsync(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."); @@ -78,6 +88,9 @@ namespace OpenIddict.Infrastructure { var type = await services.Applications.GetClientTypeAsync(application); if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && !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."); @@ -88,6 +101,9 @@ namespace OpenIddict.Infrastructure { // Confidential applications MUST authenticate to protect them from impersonation attacks. else if (!string.Equals(type, OpenIddictConstants.ClientTypes.Public)) { 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."); @@ -96,6 +112,9 @@ namespace OpenIddict.Infrastructure { } 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."); @@ -111,8 +130,13 @@ namespace OpenIddict.Infrastructure { var services = context.HttpContext.RequestServices.GetRequiredService>(); // 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.FindByIdAsync(context.ClientId); - Debug.Assert(application != null); + if (application == null) { + throw new InvalidOperationException("The token request was aborted because the client application corresponding " + + $"to the '{context.ClientId}' identifier was not found in the database."); + } var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); @@ -140,11 +164,12 @@ namespace OpenIddict.Infrastructure { public override async Task GrantRefreshToken([NotNull] GrantRefreshTokenContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - var principal = context.Ticket?.Principal; - Debug.Assert(principal != null); - - var user = await services.Users.GetUserAsync(principal); + 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."); @@ -152,20 +177,16 @@ namespace OpenIddict.Infrastructure { return; } - // Try to extract the token identifier from the authentication ticket. - // If the identifier cannot be found, the revocation check is skipped. + // Extract the token identifier from the refresh token. var identifier = context.Ticket.GetTicketId(); - if (string.IsNullOrEmpty(identifier)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); - - return; - } + 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."); @@ -183,7 +204,10 @@ namespace OpenIddict.Infrastructure { // Note: the "scopes" property stored in context.AuthenticationTicket is automatically // updated by ASOS when the client application requests a restricted scopes collection. var identity = await services.Users.CreateIdentityAsync(user, context.Ticket.GetScopes()); - Debug.Assert(identity != null); + if (identity == null) { + throw new InvalidOperationException("The token request failed 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. @@ -200,6 +224,9 @@ namespace OpenIddict.Infrastructure { var user = await services.Users.FindByNameAsync(context.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.UserName); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "Invalid credentials."); @@ -209,6 +236,9 @@ namespace OpenIddict.Infrastructure { // 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.UserName); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "The user is not allowed to sign in."); @@ -218,6 +248,9 @@ namespace OpenIddict.Infrastructure { // 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.UserName); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "Account locked out."); @@ -227,6 +260,9 @@ namespace OpenIddict.Infrastructure { // Ensure the password is valid. if (!await services.Users.CheckPasswordAsync(user, context.Password)) { + services.Logger.LogError("The token request was rejected because the password didn't match " + + "the password associated with the account '{Username}'.", context.UserName); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "Invalid credentials."); @@ -236,6 +272,9 @@ namespace OpenIddict.Infrastructure { // 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.UserName); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "Account locked out."); @@ -251,6 +290,9 @@ namespace OpenIddict.Infrastructure { // 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.UserName); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, description: "Two-factor authentication is required for this account."); @@ -267,6 +309,10 @@ namespace OpenIddict.Infrastructure { 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 ({Username}).", username); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "The 'email' scope is required."); @@ -276,7 +322,10 @@ namespace OpenIddict.Infrastructure { } var identity = await services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); - Debug.Assert(identity != null); + if (identity == null) { + throw new InvalidOperationException("The token request failed 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( diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs index 49a6a3d7..36b65690 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs @@ -6,11 +6,13 @@ 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.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider @@ -42,6 +44,9 @@ namespace OpenIddict.Infrastructure { // Retrieve the application details corresponding to the requested client_id. var application = await services.Applications.FindByIdAsync(context.ClientId); if (application == null) { + services.Logger.LogError("The introspection 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."); @@ -52,6 +57,9 @@ namespace OpenIddict.Infrastructure { // Reject non-confidential applications. var type = await services.Applications.GetClientTypeAsync(application); if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) { + services.Logger.LogError("The introspection request was rejected because the public application " + + "'{ClientId}' was not allowed to use this endpoint.", context.ClientId); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, description: "Public applications are not allowed to use the introspection endpoint."); @@ -61,6 +69,9 @@ namespace OpenIddict.Infrastructure { // Validate the client credentials. if (!await services.Applications.ValidateSecretAsync(application, context.ClientSecret)) { + services.Logger.LogError("The introspection 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."); @@ -74,11 +85,12 @@ namespace OpenIddict.Infrastructure { public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - var principal = context.Ticket?.Principal; - Debug.Assert(principal != null); - - var user = await services.Users.GetUserAsync(principal); + var user = await services.Users.GetUserAsync(context.Ticket.Principal); if (user == null) { + services.Logger.LogInformation("The token {Identifier} was declared as inactive because the " + + "corresponding user ({Username}) was not found in the database.", + context.Ticket.GetTicketId(), services.Users.GetUserName(context.Ticket.Principal)); + context.Active = false; return; @@ -90,6 +102,9 @@ namespace OpenIddict.Infrastructure { // if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid. var token = await services.Tokens.FindByIdAsync(context.Ticket.GetTicketId()); if (token == null) { + services.Logger.LogInformation("The token {Identifier} was declared as inactive because " + + "it was revoked.", context.Ticket.GetTicketId()); + context.Active = false; return; diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs index d5801fec..6eb4c543 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs @@ -36,6 +36,9 @@ namespace OpenIddict.Infrastructure { // cannot revoke 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 revocation request validation process was skipped " + + "because the client_id parameter was missing or empty."); + context.Skip(); return; @@ -97,7 +100,7 @@ namespace OpenIddict.Infrastructure { return; } - // Extract the token identifier from the authentication ticket. + // 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."); @@ -116,7 +119,7 @@ namespace OpenIddict.Infrastructure { // Revoke the refresh token. await services.Tokens.RevokeAsync(token); - services.Logger.LogInformation("The refresh token '{Identifier}' was revoked.", identifier); + services.Logger.LogInformation("The refresh token '{Identifier}' was successfully revoked.", identifier); context.Revoked = true; } diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs index f321365a..ad0f877a 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs @@ -26,9 +26,13 @@ namespace OpenIddict.Infrastructure { Debug.Assert(!context.Request.IsClientCredentialsGrantType(), "A refresh token should not be issued when using grant_type=client_credentials."); + // Note: a null value could be returned by FindByIdAsync if the user was removed after the initial + // check made by GrantAuthorizationCode/GrantRefreshToken/GrantResourceOwnerCredentials. + // In this case, throw an exception to abort the token request. var user = await services.Users.FindByIdAsync(context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier)); if (user == null) { - throw new InvalidOperationException("The user cannot be retrieved from the database."); + throw new InvalidOperationException("The token request was aborted because the user associated " + + "with the refresh token was not found in the database."); } string identifier; diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs index 68ef06e4..2c99d609 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs @@ -11,6 +11,7 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider @@ -21,6 +22,9 @@ namespace OpenIddict.Infrastructure { // Skip validation if the optional post_logout_redirect_uri // parameter was missing from the logout request. if (string.IsNullOrEmpty(context.PostLogoutRedirectUri)) { + services.Logger.LogInformation("The logout request validation process was skipped because " + + "the post_logout_redirect_uri parameter was missing."); + context.Skip(); return; @@ -28,6 +32,10 @@ namespace OpenIddict.Infrastructure { var application = await services.Applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri); if (application == null) { + services.Logger.LogError("The logout request was rejected because the client application corresponding " + + "to the specified post_logout_redirect_uri was not found in the database: " + + "'{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri); + context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, description: "Invalid post_logout_redirect_uri."); @@ -45,10 +53,15 @@ namespace OpenIddict.Infrastructure { // If the authentication cookie doesn't exist or is no longer valid, // the user agent is immediately redirected to the client application. if (context.HttpContext.User.Identities.Any(identity => identity.IsAuthenticated)) { - // Ensure that the authentication cookie contains the required NameIdentifier claim. - // If it cannot be found, ignore the logout request and continue to the next middleware. + // Ensure that the authentication cookie contains the required ClaimTypes.NameIdentifier claim. + // If it cannot be found, don't handle the logout request at this stage and continue to the next middleware. var identifier = context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(identifier)) { + services.Logger.LogWarning("The logout request was not silently processed because the mandatory " + + "ClaimTypes.NameIdentifier claim was missing from the current principal."); + + context.SkipToNextMiddleware(); + return; } @@ -57,19 +70,34 @@ namespace OpenIddict.Infrastructure { // If the token cannot be extracted, don't handle the logout request at this stage and continue to the next middleware. var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); if (principal == null) { + services.Logger.LogInformation("The logout request was not silently processed because " + + "the id_token_hint parameter was missing or invalid."); + + context.SkipToNextMiddleware(); + return; } // Ensure that the identity token corresponds to the authenticated user. If the token cannot be // validated, don't handle the logout request at this stage and continue to the next middleware. if (!principal.HasClaim(ClaimTypes.NameIdentifier, identifier)) { + services.Logger.LogWarning("The logout request was not silently processed because the principal extracted " + + "from the id_token_hint parameter didn't correspond to the logged in user."); + + context.SkipToNextMiddleware(); + return; } + services.Logger.LogInformation("The user '{Username}' was successfully logged out.", + services.Users.GetUserName(principal)); + // Delete the ASP.NET Core Identity cookies. await services.SignIn.SignOutAsync(); } + services.Logger.LogDebug("The logout request was silently processed without requiring user confirmation."); + // Redirect the user agent back to the client application. await context.HttpContext.Authentication.SignOutAsync(context.Options.AuthenticationScheme); diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs index b2a9701b..72929f43 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System; using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; @@ -19,17 +20,13 @@ namespace OpenIddict.Infrastructure { public override async Task HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - var principal = context.Ticket?.Principal; - Debug.Assert(principal != null); - - // Note: user may be null if the user has been removed. - // In this case, return a 400 response. - var user = await services.Users.GetUserAsync(principal); + // Note: user may be null if the user was removed after + // the initial check made by ValidateUserinfoRequest. + // In this case, throw an exception to abort the request. + var user = await services.Users.GetUserAsync(context.Ticket.Principal); if (user == null) { - context.Response.StatusCode = 400; - context.HandleResponse(); - - return; + throw new InvalidOperationException("The userinfo request was aborted because the user profile " + + "corresponding to the access token was not found in the database."); } // Note: "sub" is a mandatory claim. @@ -39,7 +36,7 @@ namespace OpenIddict.Infrastructure { // Only add the "preferred_username" claim if the "profile" scope was present in the access token. // 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. + // don't include the "email" scope if the username corresponds to the registered email address. if (context.Ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) { context.PreferredUsername = await services.Users.GetUserNameAsync(user);