556 lines
16 KiB
C#
556 lines
16 KiB
C#
using Microsoft.AspNetCore.Identity;
|
|
using System.Security.Claims;
|
|
|
|
namespace zero.Identity;
|
|
|
|
|
|
public partial class ZeroUserStore<TUser> :
|
|
IUserStore<TUser>,
|
|
IUserEmailStore<TUser>,
|
|
IUserLockoutStore<TUser>,
|
|
IUserPasswordStore<TUser>,
|
|
IUserClaimStore<TUser>,
|
|
IUserSecurityStampStore<TUser>,
|
|
IUserLoginStore<TUser>,
|
|
IUserAuthenticationTokenStore<TUser>,
|
|
IUserAuthenticatorKeyStore<TUser>,
|
|
IUserTwoFactorStore<TUser>,
|
|
IUserTwoFactorRecoveryCodeStore<TUser>,
|
|
IUserPhoneNumberStore<TUser>
|
|
where TUser : ZeroIdentityUser, new()
|
|
{
|
|
public ZeroUserStore(IZeroIdentityStoreDbProvider db, IdentityErrorDescriber describer = null)
|
|
{
|
|
Db = db;
|
|
ErrorDescriber = describer ?? new IdentityErrorDescriber();
|
|
}
|
|
|
|
private StringComparison _comparer = StringComparison.InvariantCultureIgnoreCase;
|
|
|
|
protected IdentityErrorDescriber ErrorDescriber { get; private set; }
|
|
|
|
protected virtual IZeroIdentityStoreDbProvider Db { get; set; }
|
|
|
|
private IdentityResult Fail(Result<TUser> result)
|
|
{
|
|
IdentityError[] errors = new IdentityError[result.Errors.Count];
|
|
|
|
int index = 0;
|
|
foreach (ResultError error in result.Errors)
|
|
{
|
|
string message = error.Message + "(key: " + error.Property + ")";
|
|
errors[index++] = new() { Code = "zero/raven/500", Description = message };
|
|
}
|
|
|
|
return IdentityResult.Failed(errors);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether an email is already reserved
|
|
/// </summary>
|
|
protected virtual async Task<bool> IsEmailReserved(TUser user, CancellationToken cancellationToken = default)
|
|
{
|
|
// TODO index
|
|
TUser existingUser = await Db.Find<TUser>(x => x.Email == user.Email && !x.IsDeleted, cancellationToken);
|
|
return existingUser != null && existingUser.Id != user.Id;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
if (await IsEmailReserved(user, cancellationToken))
|
|
{
|
|
return IdentityResult.Failed(new IdentityError
|
|
{
|
|
Code = "DuplicateEmail",
|
|
Description = $"The email address {user.Email} is already taken."
|
|
});
|
|
}
|
|
|
|
Result<TUser> result = await Db.Create(user);
|
|
|
|
if (!result.IsSuccess)
|
|
{
|
|
return Fail(result);
|
|
}
|
|
|
|
return IdentityResult.Success;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
Result<TUser> result = await Db.Delete(user);
|
|
|
|
if (!result.IsSuccess)
|
|
{
|
|
return Fail(result);
|
|
}
|
|
return IdentityResult.Success;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
TUser source = await Db.Load<TUser>(user.Id);
|
|
|
|
if (source == null)
|
|
{
|
|
return IdentityResult.Failed(new IdentityError
|
|
{
|
|
Code = "UserNotFound",
|
|
Description = $"Could not find stored user with id {user.Id}"
|
|
});
|
|
}
|
|
|
|
if (source.Email != user.Email && await IsEmailReserved(user, cancellationToken))
|
|
{
|
|
return IdentityResult.Failed(new IdentityError
|
|
{
|
|
Code = "DuplicateEmail",
|
|
Description = $"The email address {user.Email} is already taken."
|
|
});
|
|
}
|
|
|
|
Result<TUser> result = await Db.Update(user);
|
|
|
|
if (!result.IsSuccess)
|
|
{
|
|
return Fail(result);
|
|
}
|
|
|
|
return IdentityResult.Success;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
|
|
{
|
|
// TODO index
|
|
return await Db.Load<TUser>(userId);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
|
|
{
|
|
// TODO index
|
|
return await Db.Find<TUser>(x => x.Username == normalizedUserName && !x.IsDeleted, cancellationToken);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Username);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetUserIdAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Id);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetUserNameAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Username);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken)
|
|
{
|
|
user.Username = normalizedName;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken) =>
|
|
SetNormalizedUserNameAsync(user, userName, cancellationToken);
|
|
|
|
|
|
/// <inheritdoc />
|
|
void IDisposable.Dispose() { }
|
|
|
|
|
|
/*
|
|
* ****************************************************
|
|
* SECURITY STAMP
|
|
* ****************************************************
|
|
*/
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetSecurityStampAsync(TUser user, CancellationToken cancellationToken) =>
|
|
Task.FromResult(user.SecurityStamp);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken)
|
|
{
|
|
user.SecurityStamp = stamp;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/*
|
|
* ****************************************************
|
|
* EMAIL
|
|
* ****************************************************
|
|
*/
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
|
|
{
|
|
// TODO index
|
|
return await Db.Find<TUser>(x => x.Email == normalizedEmail && !x.IsDeleted, cancellationToken);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetEmailAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Email);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.IsEmailConfirmed);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.Email);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken)
|
|
{
|
|
user.Email = email.ToLowerInvariant();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
|
|
{
|
|
user.IsEmailConfirmed = confirmed;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken)
|
|
{
|
|
user.Email = normalizedEmail.ToLowerInvariant();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/*
|
|
* ****************************************************
|
|
* LOCKOUT
|
|
* ****************************************************
|
|
*/
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.AccessFailedCount);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.LockoutEnabled);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<DateTimeOffset?> GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.LockoutEnd);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
user.AccessFailedCount += 1;
|
|
return Task.FromResult(user.AccessFailedCount);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
user.AccessFailedCount = 0;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
|
|
{
|
|
user.LockoutEnabled = enabled;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
|
|
{
|
|
user.LockoutEnd = lockoutEnd;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/*
|
|
* ****************************************************
|
|
* PASSWORD
|
|
* ****************************************************
|
|
*/
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetPasswordHashAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(user.PasswordHash);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> HasPasswordAsync(TUser user, CancellationToken cancellationToken) => Task.FromResult(!user.PasswordHash.IsNullOrEmpty());
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken)
|
|
{
|
|
user.PasswordHash = passwordHash;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/*
|
|
* ****************************************************
|
|
* CLAIM
|
|
* ****************************************************
|
|
*/
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
|
{
|
|
user.Claims.AddRange(claims.Select(claim => new UserClaim(claim)));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult((IList<Claim>)user.Claims.Select(claim => claim.ToClaim()).ToList());
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
|
|
{
|
|
UserClaim userClaim = new(claim);
|
|
// TODO index
|
|
return await Db.FindAll<TUser>(x => !x.IsDeleted && x.Claims.Any(c => c.Type == userClaim.Type && c.Value == userClaim.Value), cancellationToken);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
|
{
|
|
IEnumerable<UserClaim> userClaims = claims.Select(c => new UserClaim(c)).ToList();
|
|
|
|
user.Claims = user.Claims.Except(userClaims, new UserClaimComparer()).ToList();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
|
|
{
|
|
UserClaim userClaim = new(claim);
|
|
UserClaim newUserClaim = new(newClaim);
|
|
|
|
user.Claims.Remove(userClaim);
|
|
user.Claims.Add(newUserClaim);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken)
|
|
{
|
|
user.ExternalLogins.Add(new UserExternalLogin()
|
|
{
|
|
LoginProvider = login.LoginProvider,
|
|
ProviderKey = login.ProviderKey,
|
|
ProviderDisplayName = login.ProviderDisplayName
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken)
|
|
{
|
|
UserExternalLogin login = user.ExternalLogins.FirstOrDefault(x =>
|
|
x.LoginProvider.Equals(loginProvider, _comparer) && x.ProviderKey.Equals(providerKey, _comparer));
|
|
|
|
if (login != null)
|
|
{
|
|
user.ExternalLogins.Remove(login);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult<IList<UserLoginInfo>>(user.ExternalLogins.Select(x => x.ToLoginInfo()).ToList());
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
|
|
{
|
|
// TODO index
|
|
return await Db.Find<TUser>(x => !x.IsDeleted && x.ExternalLogins.Any(l => l.LoginProvider.Equals(loginProvider, _comparer) && l.ProviderKey.Equals(providerKey, _comparer)), cancellationToken);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
|
|
{
|
|
UserToken token = user.Tokens.FirstOrDefault(x => x.LoginProvider.Equals(loginProvider, _comparer) && x.Name.Equals(name, _comparer));
|
|
|
|
if (token == null)
|
|
{
|
|
token = new() { LoginProvider = loginProvider, Name = name };
|
|
user.Tokens.Add(token);
|
|
}
|
|
|
|
token.Value = value;
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
|
{
|
|
UserToken token = user.Tokens.FirstOrDefault(x =>
|
|
x.LoginProvider.Equals(loginProvider, _comparer) && x.Name.Equals(name, _comparer));
|
|
|
|
if (token != null)
|
|
{
|
|
user.Tokens.Remove(token);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(user.Tokens.FirstOrDefault(x =>
|
|
x.LoginProvider.Equals(loginProvider, _comparer) && x.Name.Equals(name, _comparer))?.Value);
|
|
}
|
|
|
|
|
|
/// <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) =>
|
|
Task.FromResult(user.TwoFactorAuthenticatorKey);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
|
|
{
|
|
user.TwoFactorEnabled = enabled;
|
|
user.TwoFactorEnabledDate = enabled ? DateTimeOffset.Now : null;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken) =>
|
|
Task.FromResult(user.TwoFactorEnabled);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task ReplaceCodesAsync(TUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
|
|
{
|
|
user.TwoFactorRecoveryCodes = recoveryCodes;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (user == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(user));
|
|
}
|
|
if (code == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(code));
|
|
}
|
|
|
|
|
|
if (user.TwoFactorRecoveryCodes.Contains(code))
|
|
{
|
|
List<string> updatedCodes = new(user.TwoFactorRecoveryCodes.Where(s => s != code));
|
|
await ReplaceCodesAsync(user, updatedCodes, cancellationToken).ConfigureAwait(false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> CountCodesAsync(TUser user, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (user == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(user));
|
|
}
|
|
|
|
return Task.FromResult<int>(user.TwoFactorRecoveryCodes?.Count() ?? 0);
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken)
|
|
{
|
|
user.PhoneNumber = phoneNumber;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<string> GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken) =>
|
|
Task.FromResult(user.PhoneNumber);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken) =>
|
|
Task.FromResult(user.IsPhoneNumberConfirmed);
|
|
|
|
|
|
/// <inheritdoc />
|
|
public Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
|
|
{
|
|
user.IsPhoneNumberConfirmed = confirmed;
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|