From 13290991238c209e7afc16eb15e2fbcded8fc64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 4 Dec 2015 20:35:55 +0100 Subject: [PATCH] Add built-in support for ASP.NET Identity's security stamp --- src/OpenIddict.Core/OpenIddictManager.cs | 72 ++++++++++++++ src/OpenIddict.Core/OpenIddictProvider.cs | 107 +++++++++++++++------ src/OpenIddict.Mvc/OpenIddictController.cs | 48 +++------ 3 files changed, 165 insertions(+), 62 deletions(-) diff --git a/src/OpenIddict.Core/OpenIddictManager.cs b/src/OpenIddict.Core/OpenIddictManager.cs index fb5b33bb..af68d05c 100644 --- a/src/OpenIddict.Core/OpenIddictManager.cs +++ b/src/OpenIddict.Core/OpenIddictManager.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; using CryptoHelper; using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity; @@ -23,6 +26,7 @@ namespace OpenIddict { logger: services.GetService>>(), contextAccessor: services.GetService()) { Context = services.GetRequiredService().HttpContext; + Options = services.GetRequiredService>().Value; } /// @@ -30,6 +34,11 @@ namespace OpenIddict { /// public virtual HttpContext Context { get; } + /// + /// Gets the Identity options associated with the current manager. + /// + public virtual IdentityOptions Options { get; } + /// /// Gets the store associated with the current manager. /// @@ -37,6 +46,61 @@ namespace OpenIddict { get { return base.Store as IOpenIddictStore; } } + public virtual async Task CreateIdentityAsync(TUser user, IEnumerable scopes) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + + if (scopes == null) { + throw new ArgumentNullException(nameof(scopes)); + } + + var identity = new ClaimsIdentity( + OpenIddictDefaults.AuthenticationScheme, + Options.ClaimsIdentity.UserNameClaimType, + Options.ClaimsIdentity.RoleClaimType); + + identity.AddClaim(ClaimTypes.NameIdentifier, await GetUserIdAsync(user), destination: "id_token token"); + + // Resolve the username and the email address associated with the user. + var username = await GetUserNameAsync(user); + var email = await GetEmailAsync(user); + + // Only add the name claim if the "profile" scope was granted. + if (scopes.Contains(OpenIdConnectConstants.Scopes.Profile)) { + // Throw an exception if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (!scopes.Contains(OpenIdConnectConstants.Scopes.Email) && + string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { + throw new InvalidOperationException("The 'email' scope is required."); + } + + identity.AddClaim(ClaimTypes.Name, username, destination: "id_token token"); + } + + // Only add the email address if the "email" scope was granted. + if (scopes.Contains(OpenIdConnectConstants.Scopes.Email)) { + identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token"); + } + + if (SupportsUserRole) { + foreach (var role in await GetRolesAsync(user)) { + identity.AddClaim(identity.RoleClaimType, role, destination: "id_token token"); + } + } + + if (SupportsUserSecurityStamp) { + var identifier = await GetSecurityStampAsync(user); + + if (!string.IsNullOrEmpty(identifier)) { + identity.AddClaim(Options.ClaimsIdentity.SecurityStampClaimType, + identifier, destination: "id_token token"); + } + } + + return identity; + } + public virtual Task FindApplicationByIdAsync(string identifier) { return Store.FindApplicationByIdAsync(identifier, Context.RequestAborted); } @@ -46,6 +110,14 @@ namespace OpenIddict { } public virtual async Task FindClaimAsync(TUser user, string type) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrEmpty(type)) { + throw new ArgumentNullException(nameof(type)); + } + // Note: GetClaimsAsync will automatically throw an exception // if the underlying store doesn't support custom claims. diff --git a/src/OpenIddict.Core/OpenIddictProvider.cs b/src/OpenIddict.Core/OpenIddictProvider.cs index 96f5cf52..ffc89f71 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.cs +++ b/src/OpenIddict.Core/OpenIddictProvider.cs @@ -12,8 +12,10 @@ 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.OptionsModel; namespace OpenIddict { public class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { @@ -336,6 +338,35 @@ 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>(); @@ -350,6 +381,39 @@ namespace OpenIddict { context.Validate(new ClaimsPrincipal(identity)); } + 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>(); @@ -395,39 +459,22 @@ namespace OpenIddict { await manager.ResetAccessFailedCountAsync(user); } - var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); - identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user), destination: "id_token token"); - - // Resolve the username and the email address associated with the user. - var username = await manager.GetUserNameAsync(user); - var email = await manager.GetEmailAsync(user); - - // Only add the name claim if the "profile" scope was present in the token request. - if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Profile)) { - // Return an error if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (!context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email) && - string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The 'email' scope is required."); - - return; - } - - identity.AddClaim(ClaimTypes.Name, username, destination: "id_token token"); - } + // Return an error if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Profile) && + !context.Request.ContainsScope(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."); - // Only add the email address if the "email" scope was present in the token request. - if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) { - identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token"); + return; } - if (manager.SupportsUserRole) { - foreach (var name in await manager.GetRolesAsync(user)) { - identity.AddClaim(identity.RoleClaimType, name, destination: "id_token token"); - } - } + var identity = await manager.CreateIdentityAsync(user, context.Request.GetScopes()); + Debug.Assert(identity != null); context.Validate(new ClaimsPrincipal(identity)); } diff --git a/src/OpenIddict.Mvc/OpenIddictController.cs b/src/OpenIddict.Mvc/OpenIddictController.cs index aac65683..2c3b6a52 100644 --- a/src/OpenIddict.Mvc/OpenIddictController.cs +++ b/src/OpenIddict.Mvc/OpenIddictController.cs @@ -5,6 +5,7 @@ */ using System; +using System.Diagnostics; using System.Linq; using System.Security.Claims; using System.Threading; @@ -113,40 +114,23 @@ namespace OpenIddict { }); } - // Create a new ClaimsIdentity containing the claims that - // will be used to create an id_token, a token or a code. - var identity = new ClaimsIdentity(Options.AuthenticationScheme); - identity.AddClaim(ClaimTypes.NameIdentifier, await Manager.GetUserIdAsync(user)); - - // Resolve the username and the email address associated with the user. - var username = await Manager.GetUserNameAsync(user); - var email = await Manager.GetEmailAsync(user); - - // Only add the name claim if the "profile" scope was present in the authorization request. - if (request.ContainsScope(OpenIdConnectConstants.Scopes.Profile)) { - // Return an error if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (!request.ContainsScope(OpenIdConnectConstants.Scopes.Email) && - string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { - return View("Error", new OpenIdConnectMessage { - Error = OpenIdConnectConstants.Errors.InvalidRequest, - ErrorDescription = "The 'email' scope is required." - }); - } - - identity.AddClaim(ClaimTypes.Name, username, destination: "id_token token"); - } - - // Only add the email address if the "email" scope was present in the token request. - if (request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) { - identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token"); + // Return an error if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (request.ContainsScope(OpenIdConnectConstants.Scopes.Profile) && + !request.ContainsScope(OpenIdConnectConstants.Scopes.Email) && + string.Equals(await Manager.GetUserNameAsync(user), + await Manager.GetEmailAsync(user), + StringComparison.OrdinalIgnoreCase)) { + return View("Error", new OpenIdConnectMessage { + Error = OpenIdConnectConstants.Errors.InvalidRequest, + ErrorDescription = "The 'email' scope is required." + }); } - if (Manager.SupportsUserRole) { - foreach (var name in await Manager.GetRolesAsync(user)) { - identity.AddClaim(identity.RoleClaimType, name, destination: "id_token token"); - } - } + // Create a new ClaimsIdentity containing the claims that + // will be used to create an id_token, a token or a code. + var identity = await Manager.CreateIdentityAsync(user, request.GetScopes()); + Debug.Assert(identity != null); // Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application // corresponds to the client_id specified in the authorization request using