diff --git a/RavenDB.Identity.sln b/RavenDB.Identity.sln index d1c5dd8..d9bcd6e 100644 --- a/RavenDB.Identity.sln +++ b/RavenDB.Identity.sln @@ -18,9 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Readme.md = Readme.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RazorPages", "Samples\RazorPages\Sample.RazorPages.csproj", "{0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Mvc", "Samples\Mvc\Sample.Mvc.csproj", "{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Mvc", "Samples\Mvc\Sample.Mvc.csproj", "{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,10 +30,6 @@ Global {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Release|Any CPU.Build.0 = Release|Any CPU - {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Release|Any CPU.Build.0 = Release|Any CPU {8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/RavenDB.Identity/RavenDB.Identity.csproj b/RavenDB.Identity/RavenDB.Identity.csproj index c62c194..4344cae 100644 --- a/RavenDB.Identity/RavenDB.Identity.csproj +++ b/RavenDB.Identity/RavenDB.Identity.csproj @@ -30,7 +30,6 @@ - diff --git a/RavenDB.Identity/RoleStore.cs b/RavenDB.Identity/RoleStore.cs index bb81c22..b21e398 100644 --- a/RavenDB.Identity/RoleStore.cs +++ b/RavenDB.Identity/RoleStore.cs @@ -1,422 +1,422 @@ -using Microsoft.AspNetCore.Identity; -using Raven.Client; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using Raven.Client.Exceptions; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Raven.Client.Documents.Session; -using Raven.Client.Exceptions; -using Raven.Client.Documents; namespace Raven.Identity { + /// + /// + /// + /// + public class RoleMan : RoleManager + where TRole : class + { /// /// /// - /// - public class RoleMan : RoleManager - where TRole : class + /// + /// + /// + /// + /// + /// + public RoleMan(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger, IHttpContextAccessor contextAccessor) : + base(store, roleValidators, keyNormalizer, errors, logger) { - /// - /// - /// - /// - /// - /// - /// - /// - /// - public RoleMan(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger, IHttpContextAccessor contextAccessor) : - base(store, roleValidators, keyNormalizer, errors, logger) - { - } + } + } + + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role. + public class RoleStore : RoleStore + where TRole : IdentityRole + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public RoleStore(IDocumentStore store, IdentityErrorDescriber describer = null) : base(store, describer) { } + + /// + /// Creates a entity representing a role claim. + /// + /// The associated role. + /// The associated claim. + /// The role claim entity. + protected override IdentityRoleClaim CreateRoleClaim(TRole role, Claim claim) + { + return new IdentityRoleClaim { ClaimType = claim.Type, ClaimValue = claim.Value }; + } + } + + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role. + /// The type of the class representing a role claim. + public abstract class RoleStore : + IRoleClaimStore + where TRole : IdentityRole + where TRoleClaim : IdentityRoleClaim + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public RoleStore(IDocumentStore store, IdentityErrorDescriber describer = null) + { + Store = store ?? throw new ArgumentNullException(nameof(store)); + ErrorDescriber = describer ?? new IdentityErrorDescriber(); + } + + private bool _disposed; + + + /// + /// Gets the database context for this store. + /// + public IDocumentStore Store { get; private set; } + + /// + /// Gets or sets the for any error that occurred with the current operation. + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Creates a new role in a store as an asynchronous operation. + /// + /// The role to create in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (string.IsNullOrWhiteSpace(role.Name)) + { + throw new ArgumentNullException(nameof(role.Name)); + } + + using (IAsyncDocumentSession session = Store.OpenAsyncSession()) + { + var roleId = GetRavenIdFromRoleName(role.Name, Store); + await session.StoreAsync(role, roleId); + await session.SaveChangesAsync(cancellationToken); + } + return IdentityResult.Success; } /// - /// Creates a new instance of a persistence store for roles. + /// Updates a role in a store as an asynchronous operation. /// - /// The type of the class representing a role. - public class RoleStore : RoleStore - where TRole : IdentityRole + /// The role to update in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public RoleStore(IAsyncDocumentSession context, IdentityErrorDescriber describer = null) : base(context, describer) { } + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } - /// - /// Creates a entity representing a role claim. - /// - /// The associated role. - /// The associated claim. - /// The role claim entity. - protected override IdentityRoleClaim CreateRoleClaim(TRole role, Claim claim) + // TODO: Assumption is made that TRole entity is being tracked in the current session + // If not, then we'll have to Load and overwrite all properties except for Id in the loaded entity + //role.ConcurrencyStamp = Guid.NewGuid().ToString(); + try + { + using (IAsyncDocumentSession session = Store.OpenAsyncSession()) { - return new IdentityRoleClaim { ClaimType = claim.Type, ClaimValue = claim.Value }; + await session.SaveChangesAsync(cancellationToken); } + } + catch (ConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; } /// - /// Creates a new instance of a persistence store for roles. + /// Deletes a role from the store as an asynchronous operation. /// - /// The type of the class representing a role. - /// The type of the class representing a role claim. - public abstract class RoleStore : - IRoleClaimStore - where TRole : IdentityRole - where TRoleClaim : IdentityRoleClaim + /// The role to delete from the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { - /// - /// Constructs a new instance of . - /// - /// The . - /// The . - public RoleStore(IAsyncDocumentSession context, IdentityErrorDescriber describer = null) + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + using (IAsyncDocumentSession session = Store.OpenAsyncSession()) + { + session.Delete(role.Id); + } + + try + { + using (IAsyncDocumentSession session = Store.OpenAsyncSession()) { - AsyncSession = context ?? throw new ArgumentNullException(nameof(context)); - ErrorDescriber = describer ?? new IdentityErrorDescriber(); - } - - private bool _disposed; - - - /// - /// Gets the database context for this store. - /// - public IAsyncDocumentSession AsyncSession { get; private set; } - - /// - /// Gets or sets the for any error that occurred with the current operation. - /// - public IdentityErrorDescriber ErrorDescriber { get; set; } - - /// - /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. - /// - /// - /// True if changes should be automatically persisted, otherwise false. - /// - public bool AutoSaveChanges { get; set; } = true; - - /// Saves the current store. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - private async Task SaveChanges(CancellationToken cancellationToken) - { - if (AutoSaveChanges) - { - await AsyncSession.SaveChangesAsync(cancellationToken); - } - } - - /// - /// Creates a new role in a store as an asynchronous operation. - /// - /// The role to create in the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - if (string.IsNullOrWhiteSpace(role.Name)) - { - throw new ArgumentNullException(nameof(role.Name)); - } - - var roleId = GetRavenIdFromRoleName(role.Name, AsyncSession.Advanced.DocumentStore); - await AsyncSession.StoreAsync(role, roleId); - await SaveChanges(cancellationToken); - return IdentityResult.Success; - } - - /// - /// Updates a role in a store as an asynchronous operation. - /// - /// The role to update in the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - // TODO: Assumption is made that TRole entity is being tracked in the current session - // If not, then we'll have to Load and overwrite all properties except for Id in the loaded entity - //role.ConcurrencyStamp = Guid.NewGuid().ToString(); - try - { - await SaveChanges(cancellationToken); - } - catch (ConcurrencyException) - { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); - } - return IdentityResult.Success; - } - - /// - /// Deletes a role from the store as an asynchronous operation. - /// - /// The role to delete from the store. - /// The used to propagate notifications that the operation should be canceled. - /// A that represents the of the asynchronous query. - public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - AsyncSession.Delete(role.Id); - try - { - await SaveChanges(cancellationToken); - } - catch (ConcurrencyException) - { - return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); - } - return IdentityResult.Success; - } - - /// - /// Gets the ID for a role from the store as an asynchronous operation. - /// - /// The role whose ID should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the ID of the role. - public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(role.Id); - } - - /// - /// Gets the name of a role from the store as an asynchronous operation. - /// - /// The role whose name should be returned. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the name of the role. - public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - return Task.FromResult(role.Name); - } - - /// - /// Sets the name of a role in the store as an asynchronous operation. - /// - /// The role whose name should be set. - /// The name of the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - role.Name = roleName; - return Task.CompletedTask; - } - - /// - /// Finds the role who has the specified ID as an asynchronous operation. - /// - /// The role ID to look for. - /// The used to propagate notifications that the operation should be canceled. - /// A that result of the look up. - public virtual Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return AsyncSession.LoadAsync(id, cancellationToken); - } - - /// - /// Finds the role who has the specified normalized name as an asynchronous operation. - /// - /// The normalized role name to look for. - /// The used to propagate notifications that the operation should be canceled. - /// A that result of the look up. - public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - - var roleId = GetRavenIdFromRoleName(normalizedName, AsyncSession.Advanced.DocumentStore); - return AsyncSession.LoadAsync(roleId); - } - - /// - /// Get a role's normalized name as an asynchronous operation. - /// - /// The role whose normalized name should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the name of the role. - public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - return Task.FromResult(role.Name.ToLowerInvariant()); - } - - /// - /// Set a role's normalized name as an asynchronous operation. - /// - /// The role whose normalized name should be set. - /// The normalized name to set - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - //role.Name = normalizedName; - return Task.FromResult(0); - } - - /// - /// Throws if this class has been disposed. - /// - protected void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Dispose the stores - /// - public void Dispose() - { - _disposed = true; - } - - /// - /// Get the claims associated with the specified as an asynchronous operation. - /// - /// The role whose claims should be retrieved. - /// The used to propagate notifications that the operation should be canceled. - /// A that contains the claims granted to a role. - public async Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - - await Task.FromResult(0); - - return role.Claims.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList(); - } - - /// - /// Adds the given to the specified . - /// - /// The role to add the claim to. - /// The claim to add to the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - role.Claims.Add(CreateRoleClaim(role, claim)); - - return Task.FromResult(false); - } - - /// - /// Removes the given from the specified . - /// - /// The role to remove the claim from. - /// The claim to remove from the role. - /// The used to propagate notifications that the operation should be canceled. - /// The that represents the asynchronous operation. - public async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (role == null) - { - throw new ArgumentNullException(nameof(role)); - } - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - - await Task.FromResult(0); - - var claims = role.Claims.Where(c => c.ClaimValue == claim.Value).ToList(); - foreach (var c in claims) - { - role.Claims.Remove(c); - } - } - - /// - /// Creates a entity representing a role claim. - /// - /// The associated role. - /// The associated claim. - /// The role claim entity. - protected abstract TRoleClaim CreateRoleClaim(TRole role, Claim claim); - - internal static string GetRavenIdFromRoleName(string role, IDocumentStore docStore) - { - var roleCollection = docStore.Conventions.GetCollectionName(typeof(TRole)); - var prefix = docStore.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(roleCollection); - var partSeparator = docStore.Conventions.IdentityPartsSeparator; - return prefix + partSeparator + role; + await session.SaveChangesAsync(cancellationToken); } + } + catch (ConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; } + + /// + /// Gets the ID for a role from the store as an asynchronous operation. + /// + /// The role whose ID should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the ID of the role. + public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.Id); + } + + /// + /// Gets the name of a role from the store as an asynchronous operation. + /// + /// The role whose name should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the name of the role. + public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.Name); + } + + /// + /// Sets the name of a role in the store as an asynchronous operation. + /// + /// The role whose name should be set. + /// The name of the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + role.Name = roleName; + return Task.CompletedTask; + } + + /// + /// Finds the role who has the specified ID as an asynchronous operation. + /// + /// The role ID to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public virtual Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + using (IAsyncDocumentSession session = Store.OpenAsyncSession()) + { + return session.LoadAsync(id, cancellationToken); + } + } + + /// + /// Finds the role who has the specified normalized name as an asynchronous operation. + /// + /// The normalized role name to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + using (IAsyncDocumentSession session = Store.OpenAsyncSession()) + { + var roleId = GetRavenIdFromRoleName(normalizedName, Store); + return session.LoadAsync(roleId); + } + } + + /// + /// Get a role's normalized name as an asynchronous operation. + /// + /// The role whose normalized name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the name of the role. + public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + return Task.FromResult(role.Name.ToLowerInvariant()); + } + + /// + /// Set a role's normalized name as an asynchronous operation. + /// + /// The role whose normalized name should be set. + /// The normalized name to set + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + //role.Name = normalizedName; + return Task.FromResult(0); + } + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Dispose the stores + /// + public void Dispose() + { + _disposed = true; + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The role whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a role. + public async Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + + await Task.FromResult(0); + + return role.Claims.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList(); + } + + /// + /// Adds the given to the specified . + /// + /// The role to add the claim to. + /// The claim to add to the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + role.Claims.Add(CreateRoleClaim(role, claim)); + + return Task.FromResult(false); + } + + /// + /// Removes the given from the specified . + /// + /// The role to remove the claim from. + /// The claim to remove from the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + await Task.FromResult(0); + + var claims = role.Claims.Where(c => c.ClaimValue == claim.Value).ToList(); + foreach (var c in claims) + { + role.Claims.Remove(c); + } + } + + /// + /// Creates a entity representing a role claim. + /// + /// The associated role. + /// The associated claim. + /// The role claim entity. + protected abstract TRoleClaim CreateRoleClaim(TRole role, Claim claim); + + internal static string GetRavenIdFromRoleName(string role, IDocumentStore docStore) + { + var roleCollection = docStore.Conventions.GetCollectionName(typeof(TRole)); + var prefix = docStore.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(roleCollection); + var partSeparator = docStore.Conventions.IdentityPartsSeparator; + return prefix + partSeparator + role; + } + } } diff --git a/RavenDB.Identity/ServiceCollectionExtensions.cs b/RavenDB.Identity/ServiceCollectionExtensions.cs deleted file mode 100644 index fe6f763..0000000 --- a/RavenDB.Identity/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Raven.Client; -using Raven.Client.Documents; -using Raven.Client.Documents.Session; -using System; - -namespace Raven.Identity -{ - /// - /// Extends the so that RavenDB services can be registered through it. - /// - public static class ServiceCollectionExtensions - { - /// - /// Registers a RavenDB as the user store. - /// - /// The type of user. This should be a class you created derived from . - /// - /// Identity options configuration. - /// The identity builder. - public static IdentityBuilder AddRavenDbIdentity(this IServiceCollection services, Action setupAction = null) - where TUser : IdentityUser - { - return AddRavenDbIdentity(services, setupAction); - } - - /// - /// Registers a RavenDB as the user store. - /// - /// The type of user. This should be a class you created derived from . - /// The type of role. This should be a class you created derived from . - /// - /// Identity options configuration. - /// The identity builder. - public static IdentityBuilder AddRavenDbIdentity(this IServiceCollection services, Action setupAction = null) - where TUser : IdentityUser - where TRole : IdentityRole, new() - { - // Add the AspNet identity system to work with our RavenDB identity objects. - IdentityBuilder identityBuilder; - if (setupAction != null) - { - identityBuilder = services.AddIdentity(setupAction) - .AddDefaultTokenProviders(); - } - else - { - identityBuilder = services.AddIdentity() - .AddDefaultTokenProviders(); - } - - services.AddScoped, UserStore>(); - services.AddScoped, RoleStore>(); - - return identityBuilder; - } - } -} diff --git a/RavenDB.Identity/UserStore.cs b/RavenDB.Identity/UserStore.cs index 26afb11..3fdb61b 100644 --- a/RavenDB.Identity/UserStore.cs +++ b/RavenDB.Identity/UserStore.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Identity; using Raven.Client.Documents; -using Raven.Client.Documents.Operations; using Raven.Client.Documents.Operations.CompareExchange; using Raven.Client.Documents.Session; using System; @@ -12,899 +11,887 @@ using System.Threading.Tasks; namespace Raven.Identity { + /// + /// UserStore for entities in a RavenDB database. + /// + /// + /// + public class UserStore : + IUserStore, + IUserLoginStore, + IUserClaimStore, + IUserRoleStore, + IUserPasswordStore, + IUserSecurityStampStore, + IUserEmailStore, + IUserLockoutStore, + IUserTwoFactorStore, + IUserPhoneNumberStore, + IUserAuthenticatorKeyStore, + IUserAuthenticationTokenStore, + IUserTwoFactorRecoveryCodeStore, + IQueryableUserStore + where TUser : IdentityUser + where TRole : IdentityRole, new() + { + private bool _disposed; + private IDocumentStore _store; + + private const string emailReservationKeyPrefix = "emails/"; + /// - /// UserStore for entities in a RavenDB database. + /// Creates a new user store that uses the specified Raven document store. /// - /// - /// - public class UserStore : - IUserStore, - IUserLoginStore, - IUserClaimStore, - IUserRoleStore, - IUserPasswordStore, - IUserSecurityStampStore, - IUserEmailStore, - IUserLockoutStore, - IUserTwoFactorStore, - IUserPhoneNumberStore, - IUserAuthenticatorKeyStore, - IUserAuthenticationTokenStore, - IUserTwoFactorRecoveryCodeStore, - IQueryableUserStore - where TUser : IdentityUser - where TRole : IdentityRole, new() + /// + public UserStore(IDocumentStore store) { - private bool _disposed; - private readonly Func getSessionFunc; - private IAsyncDocumentSession _session; + this._store = store; + } - private const string emailReservationKeyPrefix = "emails/"; + #region IDispoable implementation - /// - /// Creates a new user store that uses the Raven document session returned from the specified session fetcher. - /// - /// The function that gets the Raven document session. - public UserStore(Func getSession) + /// + /// Disposes the user store. + /// + public void Dispose() + { + _disposed = true; + } + + #endregion + + #region IUserStore implementation + + /// + public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.Id); + } + + /// + public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.UserName); + } + + /// + public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.UserName = userName; + return Task.CompletedTask; + } + + /// + public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + // Raven string comparison queries are case-insensitive. We can just return the user name. + return Task.FromResult(user.UserName); + } + + /// + public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.UserName = normalizedName.ToLowerInvariant(); + return Task.CompletedTask; + } + + /// + public async Task CreateAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + // Make sure we have a valid email address. + if (string.IsNullOrWhiteSpace(user.Email)) + { + throw new ArgumentException("The user's email address can't be null or empty.", nameof(user)); + } + + if (string.IsNullOrEmpty(user.Id)) + { + var conventions = _store.Conventions; + var entityName = conventions.GetCollectionName(typeof(TUser)); + var prefix = conventions.TransformTypeCollectionNameToDocumentIdPrefix(entityName); + var separator = conventions.IdentityPartsSeparator; + var id = $"{prefix}{separator}{user.Email}"; + user.Id = id; + } + + if (string.IsNullOrEmpty(user.UserName)) + { + user.UserName = user.Email; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // See if the email address is already taken. + // We do this using Raven's compare/exchange functionality, which works cluster-wide. + // https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key + // + // Try to reserve a new user email + // Note: This operation takes place outside of the session transaction it is a cluster-wide reservation. + var compareExchangeKey = GetCompareExchangeKeyFromEmail(user.Email); + var reserveEmailOperation = new PutCompareExchangeValueOperation(compareExchangeKey, user.Id, 0); + var reserveEmailResult = await _store.Operations.SendAsync(reserveEmailOperation); + if (!reserveEmailResult.Successful) + { + return IdentityResult.Failed(new[] { - this.getSessionFunc = getSession; - } - - /// - /// Creates a new user store that uses the specified Raven document session. - /// - /// - public UserStore(IAsyncDocumentSession session) - { - this._session = session; - } - - #region IDispoable implementation - - /// - /// Disposes the user store. - /// - public void Dispose() - { - _disposed = true; - } - - #endregion - - #region IUserStore implementation - - /// - public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.Id); - } - - /// - public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.UserName); - } - - /// - public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.UserName = userName; - return Task.CompletedTask; - } - - /// - public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - // Raven string comparison queries are case-insensitive. We can just return the user name. - return Task.FromResult(user.UserName); - } - - /// - public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.UserName = normalizedName.ToLowerInvariant(); - return Task.CompletedTask; - } - - /// - public async Task CreateAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - // Make sure we have a valid email address. - if (string.IsNullOrWhiteSpace(user.Email)) - { - throw new ArgumentException("The user's email address can't be null or empty.", nameof(user)); - } - - if (string.IsNullOrEmpty(user.Id)) - { - var conventions = DbSession.Advanced.DocumentStore.Conventions; - var entityName = conventions.GetCollectionName(typeof(TUser)); - var prefix = conventions.TransformTypeCollectionNameToDocumentIdPrefix(entityName); - var separator = conventions.IdentityPartsSeparator; - var id = $"{prefix}{separator}{user.Email}"; - user.Id = id; - } - - if (string.IsNullOrEmpty(user.UserName)) - { - user.UserName = user.Email; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // See if the email address is already taken. - // We do this using Raven's compare/exchange functionality, which works cluster-wide. - // https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key - // - // Try to reserve a new user email - // Note: This operation takes place outside of the session transaction it is a cluster-wide reservation. - var compareExchangeKey = GetCompareExchangeKeyFromEmail(user.Email); - var reserveEmailOperation = new PutCompareExchangeValueOperation(compareExchangeKey, user.Id, 0); - var reserveEmailResult = await DbSession.Advanced.DocumentStore.Operations.SendAsync(reserveEmailOperation); - if (!reserveEmailResult.Successful) - { - return IdentityResult.Failed(new[] - { new IdentityError { Code = "DuplicateEmail", Description = $"The email address {user.Email} is already taken." } }); - } + } - // This model allows us to lookup a user by name in order to get the id - await DbSession.StoreAsync(user, cancellationToken); + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + // This model allows us to lookup a user by name in order to get the id + await session.StoreAsync(user, cancellationToken); - // Because this a a cluster-wide operation due to compare/exchange tokens, - // we need to save changes here; if we can't store the user, - // we need to roll back the email reservation. - try - { - await DbSession.SaveChangesAsync(); - } - catch (Exception) - { - // The compare/exchange email reservation is cluster-wide, outside of the session scope. - // We need to manually roll it back. - await this.DeleteUserEmailReservation(user.Email); - throw; - } - - return IdentityResult.Success; - } - - /// - public Task UpdateAsync(TUser user, CancellationToken cancellationToken) + // Because this a a cluster-wide operation due to compare/exchange tokens, + // we need to save changes here; if we can't store the user, + // we need to roll back the email reservation. + try { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(IdentityResult.Success); + await session.SaveChangesAsync(); } - - /// - public async Task DeleteAsync(TUser user, CancellationToken cancellationToken) + catch (Exception) { - ThrowIfNullDisposedCancelled(user, cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + // The compare/exchange email reservation is cluster-wide, outside of the session scope. + // We need to manually roll it back. + await this.DeleteUserEmailReservation(user.Email); + throw; + } + } - // Remove the cluster-wide compare/exchange key. - var deletionResult = await DeleteUserEmailReservation(user.Email); - if (!deletionResult.Successful) - { - return IdentityResult.Failed(new[] - { + return IdentityResult.Success; + } + + /// + public Task UpdateAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(IdentityResult.Success); + } + + /// + public async Task DeleteAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + // Remove the cluster-wide compare/exchange key. + var deletionResult = await DeleteUserEmailReservation(user.Email); + if (!deletionResult.Successful) + { + return IdentityResult.Failed(new[] + { new IdentityError { Code = "ConcurrencyFailure", Description = "Unable to delete user email compare/exchange value" } }); - } + } - // Delete the user and save it. We must save it because deleting is a cluster-wide operation. - // Only if the deletion succeeds will we remove the cluseter-wide compare/exchange key. - this.DbSession.Delete(user); - await this.DbSession.SaveChangesAsync(); - return IdentityResult.Success; - } + // Delete the user and save it. We must save it because deleting is a cluster-wide operation. + // Only if the deletion succeeds will we remove the cluseter-wide compare/exchange key. + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + session.Delete(user); + await session.SaveChangesAsync(); + } - /// - public Task FindByIdAsync(string userId, CancellationToken cancellationToken) - { - ThrowIfDisposedOrCancelled(cancellationToken); - - return DbSession.LoadAsync(userId); - } - - /// - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) - { - ThrowIfDisposedOrCancelled(cancellationToken); - if (string.IsNullOrEmpty(normalizedUserName)) - { - throw new ArgumentNullException(nameof(normalizedUserName)); - } - - var compareExchangeKey = GetCompareExchangeKeyFromEmail(normalizedUserName); - var getEmailReservationOperation = new GetCompareExchangeValueOperation(compareExchangeKey); - var emailReservationResultOrNull = await DbSession.Advanced.DocumentStore.Operations.SendAsync(getEmailReservationOperation); - var userId = emailReservationResultOrNull?.Value; - if (string.IsNullOrEmpty(userId)) - { - return null; - } - - cancellationToken.ThrowIfCancellationRequested(); - - return await FindByIdAsync(userId, cancellationToken); - } - - #endregion - - #region IUserLoginStore implementation - - /// - public async Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - if (login == null) - { - throw new ArgumentNullException(nameof(login)); - } - - if (!user.Logins.Any(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey)) - { - user.Logins.Add(login); - - var userLogin = new IdentityUserLogin - { - Id = Util.GetLoginId(login), - UserId = user.Id, - Provider = login.LoginProvider, - ProviderKey = login.ProviderKey - }; - await DbSession.StoreAsync(userLogin); - } - } - - /// - public async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - var login = new UserLoginInfo(loginProvider, providerKey, string.Empty); - string loginId = Util.GetLoginId(login); - var loginDoc = await DbSession.LoadAsync(loginId); - if (loginDoc != null) - { - DbSession.Delete(loginDoc); - } - - cancellationToken.ThrowIfCancellationRequested(); - - user.Logins.RemoveAll(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey); - } - - /// - public Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.Logins.ToIList()); - } - - /// - public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - var login = new UserLoginInfo(loginProvider, providerKey, string.Empty); - string loginId = Util.GetLoginId(login); - var loginDoc = await DbSession.LoadAsync(loginId); - if (loginDoc != null) - { - return await DbSession.LoadAsync(loginDoc.UserId); - } - - return null; - } - - #endregion - - #region IUserClaimStore implementation - - /// - public Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - IList result = user.Claims - .Select(c => new Claim(c.ClaimType, c.ClaimValue)) - .ToList(); - return Task.FromResult(result); - } - - /// - public Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.Claims.AddRange(claims.Select(c => new IdentityUserClaim { ClaimType = c.Type, ClaimValue = c.Value })); - return Task.CompletedTask; - } - - /// - public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - var indexOfClaim = user.Claims.FindIndex(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); - if (indexOfClaim != -1) - { - user.Claims.RemoveAt(indexOfClaim); - await this.AddClaimsAsync(user, new[] { newClaim }, cancellationToken); - } - } - - /// - public Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.Claims.RemoveAll(identityClaim => claims.Any(c => c.Type == identityClaim.ClaimType && c.Value == identityClaim.ClaimValue)); - return Task.CompletedTask; - } - - /// - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) - { - ThrowIfDisposedOrCancelled(cancellationToken); - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - - var list = await DbSession.Query() - .Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)) - .ToListAsync(); - return list; - } - - #endregion - - #region IUserRoleStore implementation - - /// - public async Task AddToRoleAsync(TUser user, string roleName, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - // See if we have an IdentityRole with that name. - var identityUserCollection = DbSession.Advanced.DocumentStore.Conventions.GetCollectionName(typeof(TRole)); - var prefix = DbSession.Advanced.DocumentStore.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(identityUserCollection); - var identityPartSeperator = DbSession.Advanced.DocumentStore.Conventions.IdentityPartsSeparator; - var roleNameLowered = roleName.ToLowerInvariant(); - var roleId = prefix + identityPartSeperator + roleNameLowered; - var existingRoleOrNull = await this.DbSession.LoadAsync(roleId, cancellationToken); - if (existingRoleOrNull == null) - { - ThrowIfDisposedOrCancelled(cancellationToken); - existingRoleOrNull = new TRole(); - existingRoleOrNull.Name = roleNameLowered; - await this.DbSession.StoreAsync(existingRoleOrNull, roleId, cancellationToken); - } - - // Use the real name (not normalized/uppered/lowered) of the role, as specified by the user. - var roleRealName = existingRoleOrNull.Name; - if (!user.Roles.Contains(roleRealName, StringComparer.InvariantCultureIgnoreCase)) - { - user.GetRolesList().Add(roleRealName); - } - - if (!existingRoleOrNull.Users.Contains(user.Id, StringComparer.InvariantCultureIgnoreCase)) - { - existingRoleOrNull.Users.Add(user.Id); - } - } - - /// - public async Task RemoveFromRoleAsync(TUser user, string roleName, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.GetRolesList().RemoveAll(r => string.Equals(r, roleName, StringComparison.InvariantCultureIgnoreCase)); - - var roleId = RoleStore.GetRavenIdFromRoleName(roleName, DbSession.Advanced.DocumentStore); - var roleOrNull = await DbSession.LoadAsync(roleId, cancellationToken); - if (roleOrNull != null) - { - roleOrNull.Users.Remove(user.Id); - } - } - - /// - public Task> GetRolesAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult>(new List(user.Roles)); - } - - /// - public Task IsInRoleAsync(TUser user, string roleName, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(roleName)) - { - throw new ArgumentNullException(nameof(roleName)); - } - - return Task.FromResult(user.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase)); - } - - /// - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) - { - ThrowIfDisposedOrCancelled(cancellationToken); - if (string.IsNullOrEmpty(roleName)) - { - throw new ArgumentNullException(nameof(roleName)); - } - - var users = await DbSession.Query() - .Where(u => u.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase)) - .Take(1024) - .ToListAsync(); - return users; - } - - #endregion - - #region IUserPasswordStore implementation - - /// - public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.PasswordHash = passwordHash; - return Task.CompletedTask; - } - - /// - public Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.PasswordHash); - } - - /// - public Task HasPasswordAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.PasswordHash != null); - } - - #endregion - - #region IUserSecurityStampStore implementation - - /// - public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.SecurityStamp = stamp; - return Task.CompletedTask; - } - - /// - public Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.SecurityStamp); - } - - #endregion - - #region IUserEmailStore implementation - - /// - public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken) - { - ThrowIfDisposedOrCancelled(cancellationToken); - user.Email = email ?? throw new ArgumentNullException(nameof(email)); - return Task.CompletedTask; - } - - /// - public Task GetEmailAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.Email); - } - - /// - public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.EmailConfirmed); - } - - /// - public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.EmailConfirmed = confirmed; - return Task.CompletedTask; - } - - /// - public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) - { - ThrowIfDisposedOrCancelled(cancellationToken); - - return DbSession.Query() - .FirstOrDefaultAsync(u => u.Email == normalizedEmail, cancellationToken); - } - - /// - public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - // Raven string comparison queries are case-insensitive. We can just return the user name. - return Task.FromResult(user.Email); - } - - /// - public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - if (string.IsNullOrEmpty(normalizedEmail)) - { - throw new ArgumentNullException(nameof(normalizedEmail)); - } - - user.Email = normalizedEmail.ToLowerInvariant(); // I don't like the ALL CAPS default. We're going all lower. - return Task.CompletedTask; - } - - #endregion - - #region IUserLockoutStore implementation - - /// - public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.LockoutEnd); - } - - /// - public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.LockoutEnd = lockoutEnd; - return Task.CompletedTask; - } - - /// - public Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.AccessFailedCount++; - return Task.FromResult(user.AccessFailedCount); - } - - /// - public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.AccessFailedCount = 0; - return Task.CompletedTask; - } - - /// - public Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.AccessFailedCount); - } - - /// - public Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - return Task.FromResult(user.LockoutEnabled); - } - - /// - public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.LockoutEnabled = enabled; - return Task.CompletedTask; - } - - #endregion - - #region IUserTwoFactorStore implementation - - /// - public Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.TwoFactorEnabled = enabled; - return Task.CompletedTask; - } - - /// - public Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.TwoFactorEnabled); - } - - #endregion - - #region IUserPhoneNumberStore implementation - - /// - public Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.PhoneNumber = phoneNumber; - return Task.CompletedTask; - } - - /// - public Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.PhoneNumber); - } - - /// - public Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - return Task.FromResult(user.PhoneNumberConfirmed); - } - - /// - public Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken) - { - ThrowIfNullDisposedCancelled(user, cancellationToken); - - user.PhoneNumberConfirmed = confirmed; - return Task.CompletedTask; - } - - #endregion - - #region IUserAuthenticatorKeyStore implementation - - /// - public Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) - { - user.TwoFactorAuthenticatorKey = key; - return Task.CompletedTask; - } - - /// - public Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.TwoFactorAuthenticatorKey); - } - - #endregion - - #region IUserAuthenticationTokenStore - - /// - public async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) - { - var id = IdentityUserAuthToken.GetWellKnownId(DbSession.Advanced.DocumentStore, user.Id, loginProvider, name); - ThrowIfDisposedOrCancelled(cancellationToken); - - var existingOrNull = await DbSession.LoadAsync(id); - if (existingOrNull == null) - { - existingOrNull = new IdentityUserAuthToken - { - Id = id, - LoginProvider = loginProvider, - Name = name, - UserId = user.Id, - Value = value - }; - await DbSession.StoreAsync(existingOrNull); - } - - existingOrNull.Value = value; - } - - /// - public Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - var id = IdentityUserAuthToken.GetWellKnownId(DbSession.Advanced.DocumentStore, user.Id, loginProvider, name); - DbSession.Delete(id); - - return Task.CompletedTask; - } - - /// - public async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) - { - var id = IdentityUserAuthToken.GetWellKnownId(DbSession.Advanced.DocumentStore, user.Id, loginProvider, name); - var tokenOrNull = await DbSession.LoadAsync(id); - if (tokenOrNull == null) - { - return null; - } - - return tokenOrNull.Value; - } - - /// - public Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) - { - user.TwoFactorRecoveryCodes = new List(recoveryCodes); - return Task.CompletedTask; - } - - /// - public Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken) - { - return Task.FromResult(user.TwoFactorRecoveryCodes.Remove(code)); - } - - /// - public Task CountCodesAsync(TUser user, CancellationToken cancellationToken) - { - return Task.FromResult(user.TwoFactorRecoveryCodes.Count); - } - - #endregion - - #region IQueryableUserStore - - /// - /// Gets the users as an IQueryable. - /// - public IQueryable Users => this.DbSession.Query(); - - #endregion - - /// - /// Migrates the data model from a previous version of the RavenDB.Identity framework to the v6 model. - /// This is necessary if you stored users using an RavenDB.Identity version 5 or ealier. - /// - /// - public static void MigrateToV6(IDocumentStore docStore) - { -#pragma warning disable CS0618 // Type or member is obsolete - var collectionName = docStore.Conventions.FindCollectionName(typeof(IdentityUserByUserName)); - - // Step 1: find all the old IdentityByUserName objects. - var emails = new List<(string userId, string email)>(1000); - using (var dbSession = docStore.OpenSession()) - { - var stream = dbSession.Advanced.Stream($"{collectionName}/"); -#pragma warning restore CS0618 // Type or member is obsolete - while (stream.MoveNext()) - { - var doc = stream.Current.Document; - emails.Add((userId: doc.UserId, email: doc.UserName)); - } - } - - // Step 2: store each email as a cluster-wide compare/exchange value. - foreach (var (userId, email) in emails) - { - var compareExchangeKey = GetCompareExchangeKeyFromEmail(email); - var storeOperation = new PutCompareExchangeValueOperation(compareExchangeKey, userId, 0); - var storeResult = docStore.Operations.Send(storeOperation); - if (!storeResult.Successful) - { - var exception = new Exception($"Unable to migrate to RavenDB.Identity V6. An error occurred while storing the compare/exchange value. Before running this {nameof(MigrateToV6)} again, please delete all compare/exchange values in Raven that begin with {emailReservationKeyPrefix}."); - exception.Data.Add("compareExchangeKey", compareExchangeKey); - exception.Data.Add("compareExchangeValue", userId); - throw exception; - } - } - - // Step 3: remove all IdentityUserByUserName objects. - var operation = docStore - .Operations - .Send(new DeleteByQueryOperation(new Client.Documents.Queries.IndexQuery - { - Query = $"from {collectionName}" - })); - operation.WaitForCompletion(); - } - - private IAsyncDocumentSession DbSession - { - get - { - if (_session == null) - { - _session = getSessionFunc(); - } - return _session; - } - } - - private void ThrowIfNullDisposedCancelled(TUser user, CancellationToken token) - { - if (_disposed) - { - throw new ObjectDisposedException(this.GetType().Name); - } - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - token.ThrowIfCancellationRequested(); - } - - private void ThrowIfDisposedOrCancelled(CancellationToken token) - { - if (_disposed) - { - throw new ObjectDisposedException(this.GetType().Name); - } - token.ThrowIfCancellationRequested(); - } - - private Task> DeleteUserEmailReservation(string email) - { - var key = GetCompareExchangeKeyFromEmail(email); - var store = DbSession.Advanced.DocumentStore; - - var readResult = store.Operations.Send(new GetCompareExchangeValueOperation(key)); - if (readResult == null) - { - return Task.FromResult(new CompareExchangeResult() { Successful = false }); - } - - var deleteEmailOperation = new DeleteCompareExchangeValueOperation(key, readResult.Index); - return DbSession.Advanced.DocumentStore.Operations.SendAsync(deleteEmailOperation); - } - - private static string GetCompareExchangeKeyFromEmail(string email) - { - return emailReservationKeyPrefix + email.ToLowerInvariant(); - } + return IdentityResult.Success; } + + /// + public Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + ThrowIfDisposedOrCancelled(cancellationToken); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + return session.LoadAsync(userId); + } + } + + /// + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + ThrowIfDisposedOrCancelled(cancellationToken); + if (string.IsNullOrEmpty(normalizedUserName)) + { + throw new ArgumentNullException(nameof(normalizedUserName)); + } + + var compareExchangeKey = GetCompareExchangeKeyFromEmail(normalizedUserName); + var getEmailReservationOperation = new GetCompareExchangeValueOperation(compareExchangeKey); + + var emailReservationResultOrNull = await _store.Operations.SendAsync(getEmailReservationOperation); + var userId = emailReservationResultOrNull?.Value; + if (string.IsNullOrEmpty(userId)) + { + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + + return await FindByIdAsync(userId, cancellationToken); + } + + #endregion + + #region IUserLoginStore implementation + + /// + public async Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + if (login == null) + { + throw new ArgumentNullException(nameof(login)); + } + + if (!user.Logins.Any(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey)) + { + user.Logins.Add(login); + + var userLogin = new IdentityUserLogin + { + Id = Util.GetLoginId(login), + UserId = user.Id, + Provider = login.LoginProvider, + ProviderKey = login.ProviderKey + }; + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + await session.StoreAsync(userLogin); + } + } + } + + /// + public async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + var login = new UserLoginInfo(loginProvider, providerKey, string.Empty); + string loginId = Util.GetLoginId(login); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + var loginDoc = await session.LoadAsync(loginId); + if (loginDoc != null) + { + session.Delete(loginDoc); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + user.Logins.RemoveAll(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey); + } + + /// + public Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.Logins.ToIList()); + } + + /// + public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + var login = new UserLoginInfo(loginProvider, providerKey, string.Empty); + string loginId = Util.GetLoginId(login); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + var loginDoc = await session.LoadAsync(loginId); + if (loginDoc != null) + { + return await session.LoadAsync(loginDoc.UserId); + } + } + + return null; + } + + #endregion + + #region IUserClaimStore implementation + + /// + public Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + IList result = user.Claims + .Select(c => new Claim(c.ClaimType, c.ClaimValue)) + .ToList(); + return Task.FromResult(result); + } + + /// + public Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.Claims.AddRange(claims.Select(c => new IdentityUserClaim { ClaimType = c.Type, ClaimValue = c.Value })); + return Task.CompletedTask; + } + + /// + public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + var indexOfClaim = user.Claims.FindIndex(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); + if (indexOfClaim != -1) + { + user.Claims.RemoveAt(indexOfClaim); + await this.AddClaimsAsync(user, new[] { newClaim }, cancellationToken); + } + } + + /// + public Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.Claims.RemoveAll(identityClaim => claims.Any(c => c.Type == identityClaim.ClaimType && c.Value == identityClaim.ClaimValue)); + return Task.CompletedTask; + } + + /// + public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + { + ThrowIfDisposedOrCancelled(cancellationToken); + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + return await session.Query() + .Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)) + .ToListAsync(); + } + } + + #endregion + + #region IUserRoleStore implementation + + /// + public async Task AddToRoleAsync(TUser user, string roleName, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + // See if we have an IdentityRole with that name. + var identityUserCollection = _store.Conventions.GetCollectionName(typeof(TRole)); + var prefix = _store.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(identityUserCollection); + var identityPartSeperator = _store.Conventions.IdentityPartsSeparator; + var roleNameLowered = roleName.ToLowerInvariant(); + var roleId = prefix + identityPartSeperator + roleNameLowered; + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + var existingRoleOrNull = await session.LoadAsync(roleId, cancellationToken); + if (existingRoleOrNull == null) + { + ThrowIfDisposedOrCancelled(cancellationToken); + existingRoleOrNull = new TRole(); + existingRoleOrNull.Name = roleNameLowered; + await session.StoreAsync(existingRoleOrNull, roleId, cancellationToken); + } + + // Use the real name (not normalized/uppered/lowered) of the role, as specified by the user. + var roleRealName = existingRoleOrNull.Name; + if (!user.Roles.Contains(roleRealName, StringComparer.InvariantCultureIgnoreCase)) + { + user.GetRolesList().Add(roleRealName); + } + + if (!existingRoleOrNull.Users.Contains(user.Id, StringComparer.InvariantCultureIgnoreCase)) + { + existingRoleOrNull.Users.Add(user.Id); + } + } + } + + /// + public async Task RemoveFromRoleAsync(TUser user, string roleName, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.GetRolesList().RemoveAll(r => string.Equals(r, roleName, StringComparison.InvariantCultureIgnoreCase)); + + var roleId = RoleStore.GetRavenIdFromRoleName(roleName, _store); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + var roleOrNull = await session.LoadAsync(roleId, cancellationToken); + if (roleOrNull != null) + { + roleOrNull.Users.Remove(user.Id); + } + } + } + + /// + public Task> GetRolesAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult>(new List(user.Roles)); + } + + /// + public Task IsInRoleAsync(TUser user, string roleName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(roleName)) + { + throw new ArgumentNullException(nameof(roleName)); + } + + return Task.FromResult(user.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase)); + } + + /// + public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + { + ThrowIfDisposedOrCancelled(cancellationToken); + if (string.IsNullOrEmpty(roleName)) + { + throw new ArgumentNullException(nameof(roleName)); + } + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + return await session.Query() + .Where(u => u.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase)) + .Take(1024) + .ToListAsync(); + } + } + + #endregion + + #region IUserPasswordStore implementation + + /// + public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.PasswordHash = passwordHash; + return Task.CompletedTask; + } + + /// + public Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.PasswordHash); + } + + /// + public Task HasPasswordAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.PasswordHash != null); + } + + #endregion + + #region IUserSecurityStampStore implementation + + /// + public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.SecurityStamp = stamp; + return Task.CompletedTask; + } + + /// + public Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.SecurityStamp); + } + + #endregion + + #region IUserEmailStore implementation + + /// + public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken) + { + ThrowIfDisposedOrCancelled(cancellationToken); + user.Email = email ?? throw new ArgumentNullException(nameof(email)); + return Task.CompletedTask; + } + + /// + public Task GetEmailAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.Email); + } + + /// + public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.EmailConfirmed); + } + + /// + public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.EmailConfirmed = confirmed; + return Task.CompletedTask; + } + + /// + public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + ThrowIfDisposedOrCancelled(cancellationToken); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + return session.Query() + .FirstOrDefaultAsync(u => u.Email == normalizedEmail, cancellationToken); + } + } + + /// + public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + // Raven string comparison queries are case-insensitive. We can just return the user name. + return Task.FromResult(user.Email); + } + + /// + public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + if (string.IsNullOrEmpty(normalizedEmail)) + { + throw new ArgumentNullException(nameof(normalizedEmail)); + } + + user.Email = normalizedEmail.ToLowerInvariant(); // I don't like the ALL CAPS default. We're going all lower. + return Task.CompletedTask; + } + + #endregion + + #region IUserLockoutStore implementation + + /// + public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.LockoutEnd); + } + + /// + public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.LockoutEnd = lockoutEnd; + return Task.CompletedTask; + } + + /// + public Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.AccessFailedCount++; + return Task.FromResult(user.AccessFailedCount); + } + + /// + public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.AccessFailedCount = 0; + return Task.CompletedTask; + } + + /// + public Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.AccessFailedCount); + } + + /// + public Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + return Task.FromResult(user.LockoutEnabled); + } + + /// + public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.LockoutEnabled = enabled; + return Task.CompletedTask; + } + + #endregion + + #region IUserTwoFactorStore implementation + + /// + public Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.TwoFactorEnabled = enabled; + return Task.CompletedTask; + } + + /// + public Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.TwoFactorEnabled); + } + + #endregion + + #region IUserPhoneNumberStore implementation + + /// + public Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.PhoneNumber = phoneNumber; + return Task.CompletedTask; + } + + /// + public Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.PhoneNumber); + } + + /// + public Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + return Task.FromResult(user.PhoneNumberConfirmed); + } + + /// + public Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken) + { + ThrowIfNullDisposedCancelled(user, cancellationToken); + + user.PhoneNumberConfirmed = confirmed; + return Task.CompletedTask; + } + + #endregion + + #region IUserAuthenticatorKeyStore implementation + + /// + public Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) + { + user.TwoFactorAuthenticatorKey = key; + return Task.CompletedTask; + } + + /// + public Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.TwoFactorAuthenticatorKey); + } + + #endregion + + #region IUserAuthenticationTokenStore + + /// + public async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + var id = IdentityUserAuthToken.GetWellKnownId(_store, user.Id, loginProvider, name); + ThrowIfDisposedOrCancelled(cancellationToken); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + var existingOrNull = await session.LoadAsync(id); + if (existingOrNull == null) + { + existingOrNull = new IdentityUserAuthToken + { + Id = id, + LoginProvider = loginProvider, + Name = name, + UserId = user.Id, + Value = value + }; + await session.StoreAsync(existingOrNull); + } + + existingOrNull.Value = value; + } + } + + /// + public Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + var id = IdentityUserAuthToken.GetWellKnownId(_store, user.Id, loginProvider, name); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + session.Delete(id); + } + + return Task.CompletedTask; + } + + /// + public async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + var id = IdentityUserAuthToken.GetWellKnownId(_store, user.Id, loginProvider, name); + + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + var tokenOrNull = await session.LoadAsync(id); + if (tokenOrNull == null) + { + return null; + } + + return tokenOrNull.Value; + } + } + + /// + public Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + user.TwoFactorRecoveryCodes = new List(recoveryCodes); + return Task.CompletedTask; + } + + /// + public Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken) + { + return Task.FromResult(user.TwoFactorRecoveryCodes.Remove(code)); + } + + /// + public Task CountCodesAsync(TUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.TwoFactorRecoveryCodes.Count); + } + + #endregion + + #region IQueryableUserStore + + /// + /// Gets the users as an IQueryable. + /// + public IQueryable Users + { + get + { + using (IAsyncDocumentSession session = _store.OpenAsyncSession()) + { + return session.Query(); + } + } + } + + #endregion + + private void ThrowIfNullDisposedCancelled(TUser user, CancellationToken token) + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + token.ThrowIfCancellationRequested(); + } + + private void ThrowIfDisposedOrCancelled(CancellationToken token) + { + if (_disposed) + { + throw new ObjectDisposedException(this.GetType().Name); + } + token.ThrowIfCancellationRequested(); + } + + private Task> DeleteUserEmailReservation(string email) + { + var key = GetCompareExchangeKeyFromEmail(email); + var store = _store; + + var readResult = store.Operations.Send(new GetCompareExchangeValueOperation(key)); + if (readResult == null) + { + return Task.FromResult(new CompareExchangeResult() { Successful = false }); + } + + var deleteEmailOperation = new DeleteCompareExchangeValueOperation(key, readResult.Index); + return _store.Operations.SendAsync(deleteEmailOperation); + } + + private static string GetCompareExchangeKeyFromEmail(string email) + { + return emailReservationKeyPrefix + email.ToLowerInvariant(); + } + } } diff --git a/Samples/Mvc/Sample.Mvc.csproj b/Samples/Mvc/Sample.Mvc.csproj index 6d9dfd2..f9b216a 100644 --- a/Samples/Mvc/Sample.Mvc.csproj +++ b/Samples/Mvc/Sample.Mvc.csproj @@ -11,6 +11,7 @@ + diff --git a/Samples/RazorPages/Sample.RazorPages.csproj b/Samples/RazorPages/Sample.RazorPages.csproj index 406eb1a..187fc3a 100644 --- a/Samples/RazorPages/Sample.RazorPages.csproj +++ b/Samples/RazorPages/Sample.RazorPages.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.2 @@ -11,6 +11,7 @@ +