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.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<ILogger<UserManager<TUser>>>(),
contextAccessor: services.GetService<IHttpContextAccessor>()) {
Context = services.GetRequiredService<IHttpContextAccessor>().HttpContext;
Options = services.GetRequiredService<IOptions<IdentityOptions>>().Value;
}
/// <summary>
@ -30,6 +34,11 @@ namespace OpenIddict {
/// </summary>
public virtual HttpContext Context { get; }
/// <summary>
/// Gets the Identity options associated with the current manager.
/// </summary>
public virtual IdentityOptions Options { get; }
/// <summary>
/// Gets the store associated with the current manager.
/// </summary>
@ -37,6 +46,61 @@ namespace OpenIddict {
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) {
return Store.FindApplicationByIdAsync(identifier, Context.RequestAborted);
}
@ -46,6 +110,14 @@ namespace OpenIddict {
}
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
// 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 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<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) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
@ -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<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) {
var manager = context.HttpContext.RequestServices.GetRequiredService<OpenIddictManager<TUser, TApplication>>();
@ -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));
}

48
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

Loading…
Cancel
Save