4 changed files with 259 additions and 137 deletions
@ -0,0 +1,180 @@ |
|||||
|
using System.Diagnostics; |
||||
|
using System.Linq; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using AspNet.Security.OpenIdConnect.Extensions; |
||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Builder; |
||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using Microsoft.AspNetCore.Http.Authentication; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace OpenIddict.Infrastructure { |
||||
|
public class OpenIddictMiddleware<TUser, TApplication, TAuthorization, TScope, TToken> |
||||
|
where TUser : class where TApplication : class |
||||
|
where TAuthorization : class where TScope : class where TToken : class { |
||||
|
private readonly RequestDelegate next; |
||||
|
|
||||
|
public OpenIddictMiddleware([NotNull] RequestDelegate next) { |
||||
|
this.next = next; |
||||
|
} |
||||
|
|
||||
|
public async Task Invoke([NotNull] HttpContext context) { |
||||
|
// Invoke the rest of the pipeline to allow handling
|
||||
|
// authorization, logout or token requests in user code.
|
||||
|
await next(context); |
||||
|
|
||||
|
// If the request was already handled, skip the default logic.
|
||||
|
if (context.Response.HasStarted || context.Response.StatusCode != 404) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// If the request doesn't correspond to an OpenID Connect request, ignore it.
|
||||
|
var request = context.GetOpenIdConnectRequest(); |
||||
|
if (request == null || (!request.IsAuthorizationRequest() && |
||||
|
!request.IsLogoutRequest() && |
||||
|
!request.IsTokenRequest())) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// If an OpenID Connect response was already prepared, bypass the default logic.
|
||||
|
var response = context.GetOpenIdConnectResponse(); |
||||
|
if (response != null) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Resolve the OpenIddict services from the scoped container.
|
||||
|
var services = context.RequestServices.GetRequiredService<OpenIddictServices< |
||||
|
TUser, TApplication, TAuthorization, TScope, TToken>>(); |
||||
|
|
||||
|
// Reset the response status code to allow the OpenID Connect server
|
||||
|
// middleware to apply a challenge, signin or logout response.
|
||||
|
context.Response.StatusCode = 200; |
||||
|
|
||||
|
ClaimsPrincipal principal = null; |
||||
|
|
||||
|
if (request.IsAuthorizationRequest()) { |
||||
|
// If the user is not logged in, return a challenge response.
|
||||
|
if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { |
||||
|
await context.Authentication.ChallengeAsync(); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Retrieve the profile of the logged in user. If the user
|
||||
|
// cannot be found, return a challenge response.
|
||||
|
var user = await services.Users.GetUserAsync(context.User); |
||||
|
if (user == null) { |
||||
|
await context.Authentication.ChallengeAsync(); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
services.Logger.LogInformation("The authorization request was handled without asking for user consent."); |
||||
|
|
||||
|
principal = new ClaimsPrincipal(await services.Users.CreateIdentityAsync(user, request.GetScopes())); |
||||
|
} |
||||
|
|
||||
|
else if (request.IsLogoutRequest()) { |
||||
|
// Ask ASP.NET Core Identity to delete the local and external cookies created
|
||||
|
// when the user agent is redirected from the external identity provider
|
||||
|
// after a successful authentication flow (e.g Google or Facebook).
|
||||
|
await services.SignIn.SignOutAsync(); |
||||
|
|
||||
|
await context.Authentication.SignOutAsync(services.Options.AuthenticationScheme); |
||||
|
|
||||
|
services.Logger.LogInformation("The logout request was handled without asking for user consent."); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
else if (request.IsTokenRequest()) { |
||||
|
Debug.Assert(request.IsClientCredentialsGrantType() || request.IsPasswordGrantType(), |
||||
|
"Only grant_type=client_credentials and grant_type=password requests should be handled here."); |
||||
|
|
||||
|
services.Logger.LogInformation("The token request was automatically handled."); |
||||
|
|
||||
|
if (request.IsClientCredentialsGrantType()) { |
||||
|
// Retrieve the application details corresponding to the requested client_id.
|
||||
|
// Note: this call shouldn't return a null instance, but a race condition may occur
|
||||
|
// if the application was removed after the initial check made by ValidateTokenRequest.
|
||||
|
var application = await services.Applications.FindByClientIdAsync(request.ClientId); |
||||
|
if (application == null) { |
||||
|
services.Logger.LogError("The token request was aborted because the client application " + |
||||
|
"was not found in the database: '{ClientId}'.", request.ClientId); |
||||
|
|
||||
|
await context.Authentication.ForbidAsync(services.Options.AuthenticationScheme); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var identity = new ClaimsIdentity(services.Options.AuthenticationScheme); |
||||
|
|
||||
|
// Note: the name identifier is always included in both identity and
|
||||
|
// access tokens, even if an explicit destination is not specified.
|
||||
|
identity.AddClaim(ClaimTypes.NameIdentifier, request.ClientId); |
||||
|
|
||||
|
identity.AddClaim(ClaimTypes.Name, await services.Applications.GetDisplayNameAsync(application), |
||||
|
OpenIdConnectConstants.Destinations.AccessToken, |
||||
|
OpenIdConnectConstants.Destinations.IdentityToken); |
||||
|
|
||||
|
principal = new ClaimsPrincipal(identity); |
||||
|
} |
||||
|
|
||||
|
else if (request.IsPasswordGrantType()) { |
||||
|
// Retrieve the user profile corresponding to the specified username.
|
||||
|
var user = await services.Users.FindByNameAsync(request.Username); |
||||
|
if (user == null) { |
||||
|
services.Logger.LogError("The token request was rejected because no user profile corresponding to " + |
||||
|
"the specified username was found: '{Username}'.", request.Username); |
||||
|
|
||||
|
await context.Authentication.ForbidAsync(services.Options.AuthenticationScheme); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Ensure the username/password couple is valid.
|
||||
|
if (!await services.Users.CheckPasswordAsync(user, request.Password)) { |
||||
|
services.Logger.LogError("The token request was rejected because the password didn't match " + |
||||
|
"the password associated with the account '{Username}'.", request.Username); |
||||
|
|
||||
|
if (services.Users.SupportsUserLockout) { |
||||
|
await services.Users.AccessFailedAsync(user); |
||||
|
|
||||
|
if (await services.Users.IsLockedOutAsync(user)) { |
||||
|
services.Logger.LogError("The token request was rejected because the account '{Username}' " + |
||||
|
"was locked out to prevent brute force attacks.", request.Username); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await context.Authentication.ForbidAsync(services.Options.AuthenticationScheme); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (services.Users.SupportsUserLockout) { |
||||
|
await services.Users.ResetAccessFailedCountAsync(user); |
||||
|
} |
||||
|
|
||||
|
principal = new ClaimsPrincipal(await services.Users.CreateIdentityAsync(user, request.GetScopes())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// At this stage, don't alter the response
|
||||
|
// if a sign-in operation can't be performed.
|
||||
|
if (principal != null) { |
||||
|
// Create a new authentication ticket holding the user identity.
|
||||
|
var ticket = new AuthenticationTicket( |
||||
|
principal, new AuthenticationProperties(), |
||||
|
services.Options.AuthenticationScheme); |
||||
|
|
||||
|
ticket.SetResources(request.GetResources()); |
||||
|
ticket.SetScopes(request.GetScopes()); |
||||
|
|
||||
|
await context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue