906 lines
33 KiB
C#
906 lines
33 KiB
C#
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;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Security.Claims;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Raven.Identity
|
|
{
|
|
/// <summary>
|
|
/// UserStore for entities in a RavenDB database.
|
|
/// </summary>
|
|
/// <typeparam name="TUser"></typeparam>
|
|
public class UserStore<TUser> :
|
|
IUserStore<TUser>,
|
|
IUserLoginStore<TUser>,
|
|
IUserClaimStore<TUser>,
|
|
IUserRoleStore<TUser>,
|
|
IUserPasswordStore<TUser>,
|
|
IUserSecurityStampStore<TUser>,
|
|
IUserEmailStore<TUser>,
|
|
IUserLockoutStore<TUser>,
|
|
IUserTwoFactorStore<TUser>,
|
|
IUserPhoneNumberStore<TUser>,
|
|
IUserAuthenticatorKeyStore<TUser>,
|
|
IUserAuthenticationTokenStore<TUser>,
|
|
IUserTwoFactorRecoveryCodeStore<TUser>,
|
|
IQueryableUserStore<TUser>
|
|
where TUser : IdentityUser
|
|
{
|
|
private bool _disposed;
|
|
private readonly Func<IAsyncDocumentSession> getSessionFunc;
|
|
private IAsyncDocumentSession _session;
|
|
|
|
private const string emailReservationKeyPrefix = "emails/";
|
|
|
|
/// <summary>
|
|
/// Creates a new user store that uses the Raven document session returned from the specified session fetcher.
|
|
/// </summary>
|
|
/// <param name="getSession">The function that gets the Raven document session.</param>
|
|
public UserStore(Func<IAsyncDocumentSession> getSession)
|
|
{
|
|
this.getSessionFunc = getSession;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new user store that uses the specified Raven document session.
|
|
/// </summary>
|
|
/// <param name="session"></param>
|
|
public UserStore(IAsyncDocumentSession session)
|
|
{
|
|
this._session = session;
|
|
}
|
|
|
|
#region IDispoable implementation
|
|
|
|
/// <summary>
|
|
/// Disposes the user store.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_disposed = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetUserIdAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.Id);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetUserNameAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.UserName);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.UserName = userName;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.UserName = normalizedName.ToLowerInvariant();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IdentityResult> 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 separator = conventions.IdentityPartsSeparator;
|
|
var id = $"{entityName}{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<string>(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);
|
|
|
|
// 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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(IdentityResult.Success);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IdentityResult> 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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
|
|
return DbSession.LoadAsync<TUser>(userId);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
if (string.IsNullOrEmpty(normalizedUserName))
|
|
{
|
|
throw new ArgumentNullException(nameof(normalizedUserName));
|
|
}
|
|
|
|
var compareExchangeKey = GetCompareExchangeKeyFromEmail(normalizedUserName);
|
|
var getEmailReservationOperation = new GetCompareExchangeValueOperation<string>(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
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<IdentityUserLogin>(loginId);
|
|
if (loginDoc != null)
|
|
{
|
|
DbSession.Delete(loginDoc);
|
|
}
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
user.Logins.RemoveAll(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.Logins.ToIList());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TUser> 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<IdentityUserLogin>(loginId);
|
|
if (loginDoc != null)
|
|
{
|
|
return await DbSession.LoadAsync<TUser>(loginDoc.UserId);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserClaimStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
IList<Claim> result = user.Claims
|
|
.Select(c => new Claim(c.ClaimType, c.ClaimValue))
|
|
.ToList();
|
|
return Task.FromResult(result);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.Claims.AddRange(claims.Select(c => new IdentityUserClaim { ClaimType = c.Type, ClaimValue = c.Value }));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.Claims.RemoveAll(identityClaim => claims.Any(c => c.Type == identityClaim.ClaimType && c.Value == identityClaim.ClaimValue));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
if (claim == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(claim));
|
|
}
|
|
|
|
var list = await DbSession.Query<TUser>()
|
|
.Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value))
|
|
.ToListAsync();
|
|
return list;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserRoleStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public async Task AddToRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
// See if we have an IdentityRole with that name.
|
|
var identityUserPrefix = DbSession.Advanced.DocumentStore.Conventions.GetCollectionName(typeof(IdentityRole));
|
|
var identityPartSeperator = DbSession.Advanced.DocumentStore.Conventions.IdentityPartsSeparator;
|
|
var roleNameLowered = roleName.ToLowerInvariant();
|
|
var roleId = identityUserPrefix + identityPartSeperator + roleNameLowered;
|
|
var existingRoleOrNull = await this.DbSession.LoadAsync<IdentityRole>(roleId, cancellationToken);
|
|
if (existingRoleOrNull == null)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
existingRoleOrNull = new IdentityRole(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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<IdentityRole>.GetRavenIdFromRoleName(roleName, DbSession.Advanced.DocumentStore);
|
|
var roleOrNull = await DbSession.LoadAsync<IdentityRole>(roleId, cancellationToken);
|
|
if (roleOrNull != null)
|
|
{
|
|
roleOrNull.Users.Remove(user.Id);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult<IList<string>>(new List<string>(user.Roles));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> 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));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IList<TUser>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
if (string.IsNullOrEmpty(roleName))
|
|
{
|
|
throw new ArgumentNullException(nameof(roleName));
|
|
}
|
|
|
|
var users = await DbSession.Query<TUser>()
|
|
.Where(u => u.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase))
|
|
.Take(1024)
|
|
.ToListAsync();
|
|
return users;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserPasswordStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.PasswordHash = passwordHash;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetPasswordHashAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.PasswordHash);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> HasPasswordAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.PasswordHash != null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserSecurityStampStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.SecurityStamp = stamp;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetSecurityStampAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.SecurityStamp);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserEmailStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
user.Email = email ?? throw new ArgumentNullException(nameof(email));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetEmailAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.Email);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.EmailConfirmed);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.EmailConfirmed = confirmed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfDisposedOrCancelled(cancellationToken);
|
|
|
|
return DbSession.Query<TUser>()
|
|
.FirstOrDefaultAsync(u => u.Email == normalizedEmail, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> 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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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
|
|
|
|
/// <inheritdoc />
|
|
public Task<DateTimeOffset?> GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.LockoutEnd);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.LockoutEnd = lockoutEnd;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.AccessFailedCount++;
|
|
return Task.FromResult(user.AccessFailedCount);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.AccessFailedCount = 0;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.AccessFailedCount);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
return Task.FromResult(user.LockoutEnabled);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.LockoutEnabled = enabled;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserTwoFactorStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.TwoFactorEnabled = enabled;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.TwoFactorEnabled);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserPhoneNumberStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.PhoneNumber = phoneNumber;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.PhoneNumber);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
return Task.FromResult(user.PhoneNumberConfirmed);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
|
|
{
|
|
ThrowIfNullDisposedCancelled(user, cancellationToken);
|
|
|
|
user.PhoneNumberConfirmed = confirmed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserAuthenticatorKeyStore implementation
|
|
|
|
/// <inheritdoc />
|
|
public Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken)
|
|
{
|
|
user.TwoFactorAuthenticatorKey = key;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(user.TwoFactorAuthenticatorKey);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IUserAuthenticationTokenStore
|
|
|
|
/// <inheritdoc />
|
|
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<IdentityUserAuthToken>(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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<string> 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<IdentityUserAuthToken>(id);
|
|
if (tokenOrNull == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return tokenOrNull.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task ReplaceCodesAsync(TUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
|
|
{
|
|
user.TwoFactorRecoveryCodes = new List<string>(recoveryCodes);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(user.TwoFactorRecoveryCodes.Remove(code));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> CountCodesAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(user.TwoFactorRecoveryCodes.Count);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IQueryableUserStore
|
|
|
|
/// <summary>
|
|
/// Gets the users as an IQueryable.
|
|
/// </summary>
|
|
public IQueryable<TUser> Users => this.DbSession.Query<TUser>();
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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<IdentityUserByUserName>($"{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<string>(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<CompareExchangeResult<string>> DeleteUserEmailReservation(string email)
|
|
{
|
|
var key = GetCompareExchangeKeyFromEmail(email);
|
|
var store = DbSession.Advanced.DocumentStore;
|
|
|
|
var readResult = store.Operations.Send(new GetCompareExchangeValueOperation<string>(key));
|
|
if (readResult == null)
|
|
{
|
|
return Task.FromResult(new CompareExchangeResult<string>() { Successful = false });
|
|
}
|
|
|
|
var deleteEmailOperation = new DeleteCompareExchangeValueOperation<string>(key, readResult.Index);
|
|
return DbSession.Advanced.DocumentStore.Operations.SendAsync(deleteEmailOperation);
|
|
}
|
|
|
|
private static string GetCompareExchangeKeyFromEmail(string email)
|
|
{
|
|
return emailReservationKeyPrefix + email.ToLowerInvariant();
|
|
}
|
|
}
|
|
}
|