Browse Source

Add built-in support for ASP.NET Identity's security stamp

pull/34/head
Kévin Chalet 11 years ago
parent
commit
1329099123
  1. 72
      src/OpenIddict.Core/OpenIddictManager.cs
  2. 107
      src/OpenIddict.Core/OpenIddictProvider.cs
  3. 48
      src/OpenIddict.Mvc/OpenIddictController.cs

72
src/OpenIddict.Core/OpenIddictManager.cs

@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using CryptoHelper; using CryptoHelper;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity;
@ -23,6 +26,7 @@ namespace OpenIddict {
logger: services.GetService<ILogger<UserManager<TUser>>>(), logger: services.GetService<ILogger<UserManager<TUser>>>(),
contextAccessor: services.GetService<IHttpContextAccessor>()) { contextAccessor: services.GetService<IHttpContextAccessor>()) {
Context = services.GetRequiredService<IHttpContextAccessor>().HttpContext; Context = services.GetRequiredService<IHttpContextAccessor>().HttpContext;
Options = services.GetRequiredService<IOptions<IdentityOptions>>().Value;
} }
/// <summary> /// <summary>
@ -30,6 +34,11 @@ namespace OpenIddict {
/// </summary> /// </summary>
public virtual HttpContext Context { get; } public virtual HttpContext Context { get; }
/// <summary>
/// Gets the Identity options associated with the current manager.
/// </summary>
public virtual IdentityOptions Options { get; }
/// <summary> /// <summary>
/// Gets the store associated with the current manager. /// Gets the store associated with the current manager.
/// </summary> /// </summary>
@ -37,6 +46,61 @@ namespace OpenIddict {
get { return base.Store as IOpenIddictStore<TUser, TApplication>; } get { return base.Store as IOpenIddictStore<TUser, TApplication>; }
} }
public virtual async Task<ClaimsIdentity> CreateIdentityAsync(TUser user, IEnumerable<string> 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<TApplication> FindApplicationByIdAsync(string identifier) { public virtual Task<TApplication> FindApplicationByIdAsync(string identifier) {
return Store.FindApplicationByIdAsync(identifier, Context.RequestAborted); return Store.FindApplicationByIdAsync(identifier, Context.RequestAborted);
} }
@ -46,6 +110,14 @@ namespace OpenIddict {
} }
public virtual async Task<string> FindClaimAsync(TUser user, string type) { public virtual async Task<string> 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 // Note: GetClaimsAsync will automatically throw an exception
// if the underlying store doesn't support custom claims. // if the underlying store doesn't support custom claims.

107
src/OpenIddict.Core/OpenIddictProvider.cs

@ -12,8 +12,10 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server; using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.OptionsModel;
namespace OpenIddict { namespace OpenIddict {
public class OpenIddictProvider<TUser, TApplication> : OpenIdConnectServerProvider where TUser : class where TApplication : class { public class OpenIddictProvider<TUser, TApplication> : 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<OpenIddictManager<TUser, TApplication>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<IdentityOptions>>();
// 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) { public override async Task GrantClientCredentials([NotNull] GrantClientCredentialsContext context) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>(); var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
@ -350,6 +381,39 @@ namespace OpenIddict {
context.Validate(new ClaimsPrincipal(identity)); context.Validate(new ClaimsPrincipal(identity));
} }
public override async Task GrantRefreshToken([NotNull] GrantRefreshTokenContext context) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<IdentityOptions>>();
// 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) { public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>(); var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
@ -395,39 +459,22 @@ namespace OpenIddict {
await manager.ResetAccessFailedCountAsync(user); await manager.ResetAccessFailedCountAsync(user);
} }
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); // Return an error if the username corresponds to the registered
identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user), destination: "id_token token"); // email address and if the "email" scope has not been requested.
if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Profile) &&
// Resolve the username and the email address associated with the user. !context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email) &&
var username = await manager.GetUserNameAsync(user); string.Equals(await manager.GetUserNameAsync(user),
var email = await manager.GetEmailAsync(user); await manager.GetEmailAsync(user),
StringComparison.OrdinalIgnoreCase)) {
// Only add the name claim if the "profile" scope was present in the token request. context.Reject(
if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Profile)) { error: OpenIdConnectConstants.Errors.InvalidRequest,
// Return an error if the username corresponds to the registered description: "The 'email' scope is required.");
// 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");
}
// Only add the email address if the "email" scope was present in the token request. return;
if (context.Request.ContainsScope(OpenIdConnectConstants.Scopes.Email)) {
identity.AddClaim(ClaimTypes.Email, email, destination: "id_token token");
} }
if (manager.SupportsUserRole) { var identity = await manager.CreateIdentityAsync(user, context.Request.GetScopes());
foreach (var name in await manager.GetRolesAsync(user)) { Debug.Assert(identity != null);
identity.AddClaim(identity.RoleClaimType, name, destination: "id_token token");
}
}
context.Validate(new ClaimsPrincipal(identity)); context.Validate(new ClaimsPrincipal(identity));
} }

48
src/OpenIddict.Mvc/OpenIddictController.cs

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading; using System.Threading;
@ -113,40 +114,23 @@ namespace OpenIddict {
}); });
} }
// Create a new ClaimsIdentity containing the claims that // Return an error if the username corresponds to the registered
// will be used to create an id_token, a token or a code. // email address and if the "email" scope has not been requested.
var identity = new ClaimsIdentity(Options.AuthenticationScheme); if (request.ContainsScope(OpenIdConnectConstants.Scopes.Profile) &&
identity.AddClaim(ClaimTypes.NameIdentifier, await Manager.GetUserIdAsync(user)); !request.ContainsScope(OpenIdConnectConstants.Scopes.Email) &&
string.Equals(await Manager.GetUserNameAsync(user),
// Resolve the username and the email address associated with the user. await Manager.GetEmailAsync(user),
var username = await Manager.GetUserNameAsync(user); StringComparison.OrdinalIgnoreCase)) {
var email = await Manager.GetEmailAsync(user); return View("Error", new OpenIdConnectMessage {
Error = OpenIdConnectConstants.Errors.InvalidRequest,
// Only add the name claim if the "profile" scope was present in the authorization request. ErrorDescription = "The 'email' scope is required."
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");
} }
if (Manager.SupportsUserRole) { // Create a new ClaimsIdentity containing the claims that
foreach (var name in await Manager.GetRolesAsync(user)) { // will be used to create an id_token, a token or a code.
identity.AddClaim(identity.RoleClaimType, name, destination: "id_token token"); var identity = await Manager.CreateIdentityAsync(user, request.GetScopes());
} Debug.Assert(identity != null);
}
// Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application // Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application
// corresponds to the client_id specified in the authorization request using // corresponds to the client_id specified in the authorization request using

Loading…
Cancel
Save