Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

198 lines
8.6 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using CryptoHelper;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace OpenIddict {
public class OpenIddictManager<TUser, TApplication> : UserManager<TUser> where TUser : class where TApplication : class {
public OpenIddictManager([NotNull] IServiceProvider services)
: base(services: services,
store: services.GetService<IOpenIddictStore<TUser, TApplication>>(),
optionsAccessor: services.GetService<IOptions<IdentityOptions>>(),
passwordHasher: services.GetService<IPasswordHasher<TUser>>(),
userValidators: services.GetServices<IUserValidator<TUser>>(),
passwordValidators: services.GetServices<IPasswordValidator<TUser>>(),
keyNormalizer: services.GetService<ILookupNormalizer>(),
errors: services.GetService<IdentityErrorDescriber>(),
logger: services.GetService<ILogger<UserManager<TUser>>>(),
contextAccessor: services.GetService<IHttpContextAccessor>()) {
Context = services.GetRequiredService<IHttpContextAccessor>().HttpContext;
Options = services.GetRequiredService<IOptions<IdentityOptions>>().Value;
}
/// <summary>
/// Gets the HTTP context associated with the current manager.
/// </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>
public virtual new IOpenIddictStore<TUser, TApplication> Store {
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(
OpenIdConnectServerDefaults.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 && scopes.Contains(OpenIddictConstants.Scopes.Roles)) {
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);
}
public virtual Task<TApplication> FindApplicationByLogoutRedirectUri(string url) {
return Store.FindApplicationByLogoutRedirectUri(url, Context.RequestAborted);
}
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.
return (from claim in await GetClaimsAsync(user)
where string.Equals(claim.Type, type, StringComparison.Ordinal)
select claim.Value).FirstOrDefault();
}
public virtual async Task<string> GetApplicationTypeAsync(TApplication application) {
if (application == null) {
throw new ArgumentNullException(nameof(application));
}
var type = await Store.GetApplicationTypeAsync(application, Context.RequestAborted);
// Ensure the application type returned by the store is supported by the manager.
if (!string.Equals(type, OpenIddictConstants.ApplicationTypes.Confidential, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.ApplicationTypes.Public, StringComparison.OrdinalIgnoreCase)) {
throw new InvalidOperationException("Only 'confidential' or 'public' applications are " +
"supported by the default OpenIddict manager.");
}
return type;
}
public virtual Task<string> GetDisplayNameAsync(TApplication application) {
if (application == null) {
throw new ArgumentNullException(nameof(application));
}
return Store.GetDisplayNameAsync(application, Context.RequestAborted);
}
public virtual async Task<bool> ValidateRedirectUriAsync(TApplication application, string address) {
if (application == null) {
throw new ArgumentNullException(nameof(application));
}
if (!string.Equals(address, await Store.GetRedirectUriAsync(application, Context.RequestAborted), StringComparison.Ordinal)) {
Logger.LogWarning("Client validation failed because {RedirectUri} was not a valid redirect_uri " +
"for {Client}", address, await GetDisplayNameAsync(application));
return false;
}
return true;
}
public virtual async Task<bool> ValidateSecretAsync(TApplication application, string secret) {
if (application == null) {
throw new ArgumentNullException(nameof(application));
}
if (!await this.IsConfidentialApplicationAsync(application)) {
Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications.");
return false;
}
var hash = await Store.GetHashedSecretAsync(application, Context.RequestAborted);
if (string.IsNullOrEmpty(hash)) {
Logger.LogError("Client authentication failed for {Client} because " +
"no client secret was associated with the application.");
return false;
}
if (!Crypto.VerifyHashedPassword(hash, secret)) {
Logger.LogWarning("Client authentication failed for {Client}.", await GetDisplayNameAsync(application));
return false;
}
return true;
}
}
}