Files
AspNetCore.Identity.MongoDb…/src/MongoUserOnlyStore.cs
T

1114 lines
57 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using MongoDbGenericRepository;
using MongoDB.Driver;
using AspNetCore.Identity.MongoDbCore.Extensions;
using AspNetCore.Identity.MongoDbCore.Models;
using AspNetCore.Identity.MongoDbCore.Infrastructure;
using Microsoft.AspNetCore.Identity;
namespace AspNetCore.Identity.MongoDbCore
{
/// <summary>
/// Creates a new instance of a persistence store for the specified user type.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
public class MongoUserOnlyStore<TUser> : MongoUserOnlyStore<TUser, IMongoDbContext, string>
where TUser : MongoIdentityUser<string>, new()
{
/// <summary>
/// Constructs a new instance of <see cref="MongoUserOnlyStore{TUser}"/>.
/// </summary>
/// <param name="context">The <see cref="IMongoDbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public MongoUserOnlyStore(IMongoDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
public class MongoUserOnlyStore<TUser, TContext> : MongoUserOnlyStore<TUser, TContext, string>
where TUser : MongoIdentityUser<string>
where TContext : IMongoDbContext
{
/// <summary>
/// Constructs a new instance of <see cref="MongoUserOnlyStore{TUser, TContext}"/>.
/// </summary>
/// <param name="context">The <see cref="IMongoDbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public MongoUserOnlyStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
public class MongoUserOnlyStore<TUser, TContext, TKey> : MongoUserOnlyStore<TUser, TContext, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>, IdentityUserToken<TKey>>
where TUser : MongoIdentityUser<TKey>
where TContext : IMongoDbContext
where TKey : IEquatable<TKey>
{
/// <summary>
/// Constructs a new instance of <see cref="MongoUserOnlyStore{TUser, TContext, TKey}"/>.
/// </summary>
/// <param name="context">The <see cref="IMongoDbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public MongoUserOnlyStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserClaim">The type representing a claim.</typeparam>
/// <typeparam name="TUserLogin">The type representing a user external login.</typeparam>
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
public class MongoUserOnlyStore<TUser, TContext, TKey, TUserClaim, TUserLogin, TUserToken> :
UserStoreBase<TUser, TKey, TUserClaim, TUserLogin, TUserToken>,
IUserAuthenticationTokenStore<TUser>
where TUser : MongoIdentityUser<TKey>
where TContext : IMongoDbContext
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
{
/// <summary>
/// Creates a new instance of the store.
/// </summary>
/// <param name="context">The context used to access the store.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/> used to describe store errors.</param>
public MongoUserOnlyStore(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber())
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Context = context;
}
/// <summary>
/// Gets the database context for this store.
/// </summary>
private static TContext Context { get; set; }
private static IMongoRepository _mongoRepository;
private static IMongoRepository MongoRepository
{
get
{
if (_mongoRepository == null)
{
_mongoRepository = new MongoRepository(Context);
}
return _mongoRepository;
}
}
private IMongoCollection<TUser> UsersCollection { get { return Context.GetCollection<TUser>(); } }
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges { get; set; } = true;
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
protected Task SaveChanges(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// Creates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the creation operation.</returns>
public async override Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
await UsersCollection.InsertOneAsync(user);
await SaveChanges(cancellationToken);
return IdentityResult.Success;
}
/// <summary>
/// Updates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public async override Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var oldStamp = user.ConcurrencyStamp;
user.ConcurrencyStamp = Guid.NewGuid().ToString();
var updateRes = await UsersCollection.ReplaceOneAsync(x => x.Id.Equals(user.Id)
&& x.ConcurrencyStamp.Equals(oldStamp),
user);
if(updateRes.ModifiedCount == 0)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Deletes the specified <paramref name="user"/> from the user store.
/// </summary>
/// <param name="user">The user to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public async override Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
user.Claims.Clear();
user.Roles.Clear();
user.Logins.Clear();
user.Tokens.Clear();
var oldStamp = user.ConcurrencyStamp;
user.ConcurrencyStamp = Guid.NewGuid().ToString();
var deleteRes = await UsersCollection.DeleteOneAsync(x => x.Id.Equals(user.Id)
&& x.ConcurrencyStamp.Equals(oldStamp));
if (deleteRes.DeletedCount == 0)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Finds and returns a user, if any, who has the specified <paramref name="userId"/>.
/// </summary>
/// <param name="userId">The user ID to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userId"/> if it exists.
/// </returns>
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var id = ConvertIdFromString(userId);
return MongoRepository.GetByIdAsync<TUser, TKey>(id);
}
/// <summary>
/// Finds and returns a user, if any, who has the specified normalized user name.
/// </summary>
/// <param name="normalizedUserName">The normalized user name to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="normalizedUserName"/> if it exists.
/// </returns>
public override Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return MongoRepository.GetOneAsync<TUser, TKey>(u => u.NormalizedUserName == normalizedUserName);
}
/// <summary>
/// A navigation property for the users the store contains.
/// </summary>
public override IQueryable<TUser> Users
{
get { return UsersCollection.AsQueryable(); }
}
/// <summary>
/// Return a user with the matching userId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user if it exists.</returns>
protected override Task<TUser> FindUserAsync(TKey userId, CancellationToken cancellationToken)
{
return MongoRepository.GetOneAsync<TUser, TKey>(u => u.Id.Equals(userId));
}
/// <summary>
/// Return a user login with the matching userId, provider, providerKey if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
protected override Task<TUserLogin> FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
var user = MongoRepository.GetOne<TUser, TKey>(x => x.Id.Equals(userId) && x.Logins.Any(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey));
if (user != null)
{
return Task.FromResult((TUserLogin)user.GetUserLogin(loginProvider, providerKey));
}
return Task.FromResult(default(TUserLogin));
}
/// <summary>
/// Return a user login with provider, providerKey if it exists.
/// </summary>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
protected override Task<TUserLogin> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
var user = MongoRepository.GetOne<TUser, TKey>(x => x.Logins.Any(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey));
if (user != null)
{
return Task.FromResult((TUserLogin)user.GetUserLogin(loginProvider, providerKey));
}
return Task.FromResult(default(TUserLogin));
}
#pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
/// <summary>
/// Get the claims associated with the specified <paramref name="user"/> as an asynchronous operation.
/// </summary>
/// <param name="user">The user whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a user.</returns>
public async override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
#pragma warning restore CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return user.Claims.Select(e => e.ToClaim()).ToList();
}
/// <summary>
/// Adds the <paramref name="claims"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the claim to.</param>
/// <param name="claims">The claim to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claims == null)
{
throw new ArgumentNullException(nameof(claims));
}
var addedSome = false;
foreach (var claim in claims)
{
if (user.AddClaim(claim))
{
addedSome |= true;
}
}
if (addedSome)
{
var success = MongoRepository.UpdateOne<TUser, TKey, List<MongoClaim>>(user, p => p.Claims, user.Claims);
if (!success)
{
throw new Exception($"Failed to add claims to user {user.Id.ToString()}");
}
}
return Task.FromResult(false);
}
/// <summary>
/// Replaces the <paramref name="claim"/> on the specified <paramref name="user"/>, with the <paramref name="newClaim"/>.
/// </summary>
/// <param name="user">The user to replace the claim on.</param>
/// <param name="claim">The claim replace.</param>
/// <param name="newClaim">The new claim replacing the <paramref name="claim"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claim == null)
{
throw new ArgumentNullException(nameof(claim));
}
if (newClaim == null)
{
throw new ArgumentNullException(nameof(newClaim));
}
if (user.ReplaceClaim(claim, newClaim))
{
await MongoRepository.UpdateOneAsync<TUser, TKey, List<MongoClaim>>(user, e => e.Claims, user.Claims);
}
}
/// <summary>
/// Removes the <paramref name="claims"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the claims from.</param>
/// <param name="claims">The claim to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claims == null)
{
throw new ArgumentNullException(nameof(claims));
}
if (user.RemoveClaims(claims))
{
await MongoRepository.UpdateOneAsync<TUser, TKey, List<MongoClaim>>(user, e => e.Claims, user.Claims);
}
}
/// <summary>
/// Adds the <paramref name="login"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the login to.</param>
/// <param name="login">The login to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddLoginAsync(TUser user, UserLoginInfo login,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (login == null)
{
throw new ArgumentNullException(nameof(login));
}
if (user.AddLogin(login))
{
MongoRepository.UpdateOne<TUser, TKey, List<UserLoginInfo>>(user, e => e.Logins, user.Logins);
}
return Task.FromResult(false);
}
#pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
/// <summary>
/// Removes the <paramref name="loginProvider"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the login from.</param>
/// <param name="loginProvider">The login to remove from the user.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey,
#pragma warning restore CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var entry = user.Logins.FirstOrDefault(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey);
if (entry != null)
{
user.RemoveLogin(entry);
}
}
#pragma warning disable CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
/// <summary>
/// Retrieves the associated logins for the specified <param ref="user"/>.
/// </summary>
/// <param name="user">The user whose associated logins to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing a list of <see cref="UserLoginInfo"/> for the specified <paramref name="user"/>, if any.
/// </returns>
public async override Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
#pragma warning restore CS1998 // Cette méthode async n'a pas d'opérateur 'await' et elle s'exécutera de façon synchrone
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return user.Logins.ToList();
}
/// <summary>
/// Retrieves the user associated with the specified login provider and login provider key.
/// </summary>
/// <param name="loginProvider">The login provider who provided the <paramref name="providerKey"/>.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing the user, if any which matched the specified login provider and key.
/// </returns>
public async override Task<TUser> FindByLoginAsync(string loginProvider, string providerKey,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken);
if (userLogin != null)
{
return await FindUserAsync(userLogin.UserId, cancellationToken);
}
return null;
}
/// <summary>
/// Gets the user, if any, associated with the specified, normalized email address.
/// </summary>
/// <param name="normalizedEmail">The normalized email address to return the user for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address.
/// </returns>
public override Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return MongoRepository.GetOneAsync<TUser, TKey>(u => u.NormalizedEmail == normalizedEmail);
}
/// <summary>
/// Retrieves all users with the specified claim.
/// </summary>
/// <param name="claim">The claim whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim.
/// </returns>
public async override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (claim == null)
{
throw new ArgumentNullException(nameof(claim));
}
var filter = Builders<TUser>.Filter.ElemMatch(x => x.Claims, userClaims => userClaims.Value.Equals(claim.Value)
&& userClaims.Type.Equals(claim.Type));
var cursor = UsersCollection.Find(filter);
var res = await cursor.ToListAsync();
return res;
}
#region Token Management
/// <summary>
/// Find a user token if it exists.
/// </summary>
/// <param name="user">The token owner.</param>
/// <param name="loginProvider">The login provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user token if it exists.</returns>
protected override Task<TUserToken> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
return Task.FromResult((TUserToken)user.GetToken(loginProvider, name));
}
/// <summary>
/// Add a new user token.
/// </summary>
/// <param name="token">The token to be added.</param>
/// <returns></returns>
protected override Task AddUserTokenAsync(TUserToken token)
{
var user = MongoRepository.GetById<TUser, TKey>(token.UserId);
if (user != null)
{
if (user.AddUserToken(token))
{
MongoRepository.UpdateOne<TUser, TKey, List<Token>>(user, e => e.Tokens, user.Tokens);
}
}
return Task.CompletedTask;
}
/// <summary>
/// Remove a new user token.
/// </summary>
/// <param name="token">The token to be removed.</param>
/// <returns></returns>
protected override Task RemoveUserTokenAsync(TUserToken token)
{
var user = MongoRepository.GetById<TUser, TKey>(token.UserId);
if (user != null)
{
if (user.RemoveUserToken(token))
{
MongoRepository.UpdateOne<TUser, TKey, List<Token>>(user, e => e.Tokens, user.Tokens);
}
}
return Task.CompletedTask;
}
#endregion Token Management
#region UserStoreBase overrides
/// <summary>
/// Sets the given <paramref name="userName" /> for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose name should be set.</param>
/// <param name="userName">The user name to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.UserName != userName)
{
user.UserName = userName;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.UserName, user.UserName);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the given normalized name for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose name should be set.</param>
/// <param name="normalizedName">The normalized name to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.NormalizedUserName != normalizedName)
{
user.NormalizedUserName = normalizedName;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.NormalizedUserName, user.NormalizedUserName);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the password hash for a user.
/// </summary>
/// <param name="user">The user to set the password hash for.</param>
/// <param name="passwordHash">The password hash to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.PasswordHash != passwordHash)
{
user.PasswordHash = passwordHash;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.PasswordHash, user.PasswordHash);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the flag indicating whether the specified <paramref name="user"/>'s email address has been confirmed or not.
/// </summary>
/// <param name="user">The user whose email confirmation status should be set.</param>
/// <param name="confirmed">A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public override Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.EmailConfirmed != confirmed)
{
user.EmailConfirmed = confirmed;
MongoRepository.UpdateOne<TUser, TKey, bool>(user, e => e.EmailConfirmed, user.EmailConfirmed);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the <paramref name="email"/> address for a <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose email should be set.</param>
/// <param name="email">The email to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public override Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.Email != email)
{
user.Email = email;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.Email, user.Email);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the normalized email for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose email address to set.</param>
/// <param name="normalizedEmail">The normalized email to set for the specified <paramref name="user"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public override Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.NormalizedEmail != normalizedEmail)
{
user.NormalizedEmail = normalizedEmail;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.NormalizedEmail, user.NormalizedEmail);
}
user.NormalizedEmail = normalizedEmail;
return Task.CompletedTask;
}
/// <summary>
/// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user.
/// </summary>
/// <param name="user">The user whose lockout date should be set.</param>
/// <param name="lockoutEnd">The <see cref="DateTimeOffset"/> after which the <paramref name="user"/>'s lockout should end.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.LockoutEnd != lockoutEnd)
{
user.LockoutEnd = lockoutEnd;
MongoRepository.UpdateOne<TUser, TKey, DateTimeOffset?>(user, e => e.LockoutEnd, user.LockoutEnd);
}
return Task.CompletedTask;
}
/// <summary>
/// Records that a failed access has occurred, incrementing the failed access count.
/// </summary>
/// <param name="user">The user whose cancellation count should be incremented.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the incremented failed access count.</returns>
public override Task<int> IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
user.AccessFailedCount++;
MongoRepository.UpdateOne<TUser, TKey, int>(user, e => e.AccessFailedCount, user.AccessFailedCount);
return Task.FromResult(user.AccessFailedCount);
}
/// <summary>
/// Resets a user's failed access count.
/// </summary>
/// <param name="user">The user whose failed access count should be reset.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
/// <remarks>This is typically called after the account is successfully accessed.</remarks>
public override Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.AccessFailedCount != 0)
{
user.AccessFailedCount = 0;
MongoRepository.UpdateOne<TUser, TKey, int>(user, e => e.AccessFailedCount, user.AccessFailedCount);
}
return Task.CompletedTask;
}
/// <summary>
/// Set the flag indicating if the specified <paramref name="user"/> can be locked out..
/// </summary>
/// <param name="user">The user whose ability to be locked out should be set.</param>
/// <param name="enabled">A flag indicating if lock out can be enabled for the specified <paramref name="user"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.LockoutEnabled != enabled)
{
user.LockoutEnabled = enabled;
MongoRepository.UpdateOne<TUser, TKey, bool>(user, e => e.LockoutEnabled, user.LockoutEnabled);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the telephone number for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose telephone number should be set.</param>
/// <param name="phoneNumber">The telephone number to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.PhoneNumber != phoneNumber)
{
user.PhoneNumber = phoneNumber;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.PhoneNumber, user.PhoneNumber);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets a flag indicating if the specified <paramref name="user"/>'s phone number has been confirmed..
/// </summary>
/// <param name="user">The user whose telephone number confirmation status should be set.</param>
/// <param name="confirmed">A flag indicating whether the user's telephone number has been confirmed.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.PhoneNumberConfirmed != confirmed)
{
user.PhoneNumberConfirmed = confirmed;
MongoRepository.UpdateOne<TUser, TKey, bool>(user, e => e.PhoneNumberConfirmed, user.PhoneNumberConfirmed);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the provided security <paramref name="stamp"/> for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose security stamp should be set.</param>
/// <param name="stamp">The security stamp to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (stamp == null)
{
throw new ArgumentNullException(nameof(stamp));
}
if (user.SecurityStamp != stamp)
{
user.SecurityStamp = stamp;
MongoRepository.UpdateOne<TUser, TKey, string>(user, e => e.SecurityStamp, user.SecurityStamp);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets a flag indicating whether the specified <paramref name="user"/> has two factor authentication enabled or not,
/// as an asynchronous operation.
/// </summary>
/// <param name="user">The user whose two factor authentication enabled status should be set.</param>
/// <param name="enabled">A flag indicating whether the specified <paramref name="user"/> has two factor authentication enabled.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.TwoFactorEnabled != enabled)
{
user.TwoFactorEnabled = enabled;
MongoRepository.UpdateOne<TUser, TKey, bool>(user, e => e.TwoFactorEnabled, user.TwoFactorEnabled);
}
return Task.CompletedTask;
}
/// <summary>
/// Sets the token value for a particular user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="loginProvider">The authentication provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="value">The value of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var token = await FindTokenAsync(user, loginProvider, name, cancellationToken);
if (token == null)
{
if (user.AddUserToken(CreateUserToken(user, loginProvider, name, value)))
{
MongoRepository.UpdateOne<TUser, TKey, List<Token>>(user, e => e.Tokens, user.Tokens);
}
//await AddUserTokenAsync(CreateUserToken(user, loginProvider, name, value));
}
else
{
if (user.SetToken(token, value))
{
MongoRepository.UpdateOne<TUser, TKey, List<Token>>(user, e => e.Tokens, user.Tokens);
}
}
}
/// <summary>
/// Deletes a token for a user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="loginProvider">The authentication provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken);
if (entry != null)
{
if (user.RemoveUserToken(entry))
{
MongoRepository.UpdateOne<TUser, TKey, List<Token>>(user, e => e.Tokens, user.Tokens);
}
}
}
/// <summary>
/// Returns the token value.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="loginProvider">The authentication provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override async Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken);
return entry?.Value;
}
private const string InternalLoginProvider = "[AspNetUserStore]";
private const string AuthenticatorKeyTokenName = "AuthenticatorKey";
private const string RecoveryCodeTokenName = "RecoveryCodes";
/// <summary>
/// Sets the authenticator key for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user whose authenticator key should be set.</param>
/// <param name="key">The authenticator key to set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken)
=> SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken);
/// <summary>
/// Get the authenticator key for the specified <paramref name="user" />.
/// </summary>
/// <param name="user">The user whose security stamp should be set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the security stamp for the specified <paramref name="user"/>.</returns>
public override Task<string> GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken)
=> GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken);
/// <summary>
/// Returns how many recovery code are still valid for a user.
/// </summary>
/// <param name="user">The user who owns the recovery code.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The number of valid recovery codes for the user..</returns>
public override async Task<int> CountCodesAsync(TUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? "";
if (mergedCodes.Length > 0)
{
return mergedCodes.Split(';').Length;
}
return 0;
}
/// <summary>
/// Updates the recovery codes for the user while invalidating any previous recovery codes.
/// </summary>
/// <param name="user">The user to store new recovery codes for.</param>
/// <param name="recoveryCodes">The new recovery codes for the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The new recovery codes for the user.</returns>
public override Task ReplaceCodesAsync(TUser user, IEnumerable<string> recoveryCodes, CancellationToken cancellationToken)
{
var mergedCodes = string.Join(";", recoveryCodes);
return SetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken);
}
/// <summary>
/// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid
/// once, and will be invalid after use.
/// </summary>
/// <param name="user">The user who owns the recovery code.</param>
/// <param name="code">The recovery code to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>True if the recovery code was found for the user.</returns>
public override async Task<bool> RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (code == null)
{
throw new ArgumentNullException(nameof(code));
}
var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? "";
var splitCodes = mergedCodes.Split(';');
if (splitCodes.Contains(code))
{
var updatedCodes = new List<string>(splitCodes.Where(s => s != code));
await ReplaceCodesAsync(user, updatedCodes, cancellationToken);
return true;
}
return false;
}
#endregion
}
}