diff --git a/Identity.sln b/Identity.sln index f5ad78907b..dd2aeb9d02 100644 --- a/Identity.sln +++ b/Identity.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2013 -VisualStudioVersion = 12.0.21126.0 +VisualStudioVersion = 12.0.21005.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0F647068-6602-4E24-B1DC-8ED91481A50A}" EndProject @@ -46,11 +46,11 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {77CEDA6C-A833-455D-8357-649BFD944724} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} = {0F647068-6602-4E24-B1DC-8ED91481A50A} - {6211450F-FFB8-431F-84E2-9A7620875260} = {77CEDA6C-A833-455D-8357-649BFD944724} - {D32483A4-B617-480C-81E6-49CD596B9A34} = {77CEDA6C-A833-455D-8357-649BFD944724} + {77CEDA6C-A833-455D-8357-649BFD944724} = {0F647068-6602-4E24-B1DC-8ED91481A50A} {B72401D7-47F6-4A98-89D5-CCBFEFC5B2B8} = {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} {E52361C9-1F0B-4229-86A0-E5C7C12A5429} = {F6B0C0E9-C346-49D0-B583-95B6CE04BB1B} + {6211450F-FFB8-431F-84E2-9A7620875260} = {77CEDA6C-A833-455D-8357-649BFD944724} + {D32483A4-B617-480C-81E6-49CD596B9A34} = {77CEDA6C-A833-455D-8357-649BFD944724} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Identity/IClaimsIdentityFactory.cs b/src/Microsoft.AspNet.Identity/IClaimsIdentityFactory.cs new file mode 100644 index 0000000000..bd5562df13 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IClaimsIdentityFactory.cs @@ -0,0 +1,29 @@ +using System; +#if NET45 +using System.Security.Claims; +#endif +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface for creating a ClaimsIdentity from an IUser + /// + /// + /// + public interface IClaimsIdentityFactory + where TUser : class, IUser + where TKey : IEquatable + { +#if NET45 + /// + /// Create a ClaimsIdentity from an user using a UserManager + /// + /// + /// + /// + /// + Task Create(UserManager manager, TUser user, string authenticationType); +#endif + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IIdentityMessageService.cs b/src/Microsoft.AspNet.Identity/IIdentityMessageService.cs new file mode 100644 index 0000000000..5861580a68 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IIdentityMessageService.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Expose a way to send messages (email/txt) + /// + public interface IIdentityMessageService + { + /// + /// This method should send the message + /// + /// + /// + Task Send(IdentityMessage message); + } + + /// + /// Represents a message + /// + public class IdentityMessage + { + /// + /// Destination, i.e. To email, or SMS phone number + /// + public virtual string Destination { get; set; } + + /// + /// Subject + /// + public virtual string Subject { get; set; } + + /// + /// Message contents + /// + public virtual string Body { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IIdentityValidator.cs b/src/Microsoft.AspNet.Identity/IIdentityValidator.cs new file mode 100644 index 0000000000..a0ddf1e319 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IIdentityValidator.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Used to validate an item + /// + /// + public interface IIdentityValidator + { + /// + /// Validate the item + /// + /// + /// + Task Validate(T item); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IPasswordHasher.cs b/src/Microsoft.AspNet.Identity/IPasswordHasher.cs new file mode 100644 index 0000000000..713fb44134 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IPasswordHasher.cs @@ -0,0 +1,23 @@ +namespace Microsoft.AspNet.Identity +{ + /// + /// Abstraction for password hashing methods + /// + public interface IPasswordHasher + { + /// + /// Hash a password + /// + /// + /// + string HashPassword(string password); + + /// + /// Verify that a password matches the hashed password + /// + /// + /// + /// + PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IQueryableRoleStore.cs b/src/Microsoft.AspNet.Identity/IQueryableRoleStore.cs new file mode 100644 index 0000000000..93c30f3a92 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IQueryableRoleStore.cs @@ -0,0 +1,25 @@ +using System.Linq; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface that exposes an IQueryable roles + /// + /// + public interface IQueryableRoleStore : IQueryableRoleStore where TRole : IRole + { + } + + /// + /// Interface that exposes an IQueryable roles + /// + /// + /// + public interface IQueryableRoleStore : IRoleStore where TRole : IRole + { + /// + /// IQueryable users + /// + IQueryable Roles { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IQueryableUserStore.cs b/src/Microsoft.AspNet.Identity/IQueryableUserStore.cs new file mode 100644 index 0000000000..d8e26b7e6b --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IQueryableUserStore.cs @@ -0,0 +1,25 @@ +using System.Linq; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface that exposes an IQueryable users + /// + /// + public interface IQueryableUserStore : IQueryableUserStore where TUser : class, IUser + { + } + + /// + /// Interface that exposes an IQueryable users + /// + /// + /// + public interface IQueryableUserStore : IUserStore where TUser : class, IUser + { + /// + /// IQueryable users + /// + IQueryable Users { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IRole.cs b/src/Microsoft.AspNet.Identity/IRole.cs new file mode 100644 index 0000000000..8cebb0189c --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IRole.cs @@ -0,0 +1,19 @@ +namespace Microsoft.AspNet.Identity +{ + /// + /// Mimimal set of data needed to persist role data + /// + /// + public interface IRole + { + /// + /// Id of the role + /// + TKey Id { get; } + + /// + /// Name of the role + /// + string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IRoleStore.cs b/src/Microsoft.AspNet.Identity/IRoleStore.cs new file mode 100644 index 0000000000..8286b56b46 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IRoleStore.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface that exposes basic role management + /// + /// + /// + public interface IRoleStore : IDisposable where TRole : IRole + { + /// + /// Insert a new role + /// + /// + /// + Task Create(TRole role); + + /// + /// Update a role + /// + /// + /// + Task Update(TRole role); + + /// + /// Delete a role + /// + /// + /// + Task Delete(TRole role); + + /// + /// Finds a role by id + /// + /// + /// + Task FindById(TKey roleId); + + /// + /// Find a role by name + /// + /// + /// + Task FindByName(string roleName); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUser.cs b/src/Microsoft.AspNet.Identity/IUser.cs new file mode 100644 index 0000000000..432c76534e --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUser.cs @@ -0,0 +1,19 @@ +namespace Microsoft.AspNet.Identity +{ + /// + /// Minimal interface for a user with id and username + /// + /// + public interface IUser + { + /// + /// Unique key for the user + /// + TKey Id { get; } + + /// + /// Unique username + /// + string UserName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserClaimStore.cs b/src/Microsoft.AspNet.Identity/IUserClaimStore.cs new file mode 100644 index 0000000000..b1bffbbc59 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserClaimStore.cs @@ -0,0 +1,41 @@ +#if NET45 + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores user specific claims + /// + /// + /// + public interface IUserClaimStore : IUserStore where TUser : class, IUser + { + /// + /// Returns the claims for the user with the issuer set + /// + /// + /// + Task> GetClaims(TUser user); + + /// + /// Add a new user claim + /// + /// + /// + /// + Task AddClaim(TUser user, Claim claim); + + /// + /// Remove a user claim + /// + /// + /// + /// + Task RemoveClaim(TUser user, Claim claim); + } +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserEmailStore.cs b/src/Microsoft.AspNet.Identity/IUserEmailStore.cs new file mode 100644 index 0000000000..8a631f79cb --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserEmailStore.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores a user's email + /// + /// + /// + public interface IUserEmailStore : IUserStore where TUser : class, IUser + { + /// + /// Set the user email + /// + /// + /// + /// + Task SetEmail(TUser user, string email); + + /// + /// Get the user email + /// + /// + /// + Task GetEmail(TUser user); + + /// + /// Returns true if the user email is confirmed + /// + /// + /// + Task GetEmailConfirmed(TUser user); + + /// + /// Sets whether the user email is confirmed + /// + /// + /// + /// + Task SetEmailConfirmed(TUser user, bool confirmed); + + /// + /// Returns the user associated with this email + /// + /// + /// + Task FindByEmail(string email); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserLockoutStore.cs b/src/Microsoft.AspNet.Identity/IUserLockoutStore.cs new file mode 100644 index 0000000000..813fcc12c0 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserLockoutStore.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores information which can be used to implement account lockout, including access failures and lockout status + /// + /// + /// + public interface IUserLockoutStore : IUserStore where TUser : class, IUser + { + /// + /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered + /// not locked out. + /// + /// + /// + Task GetLockoutEndDate(TUser user); + + /// + /// Locks a user out until the specified end date (set to a past date, to unlock a user) + /// + /// + /// + /// + Task SetLockoutEndDate(TUser user, DateTimeOffset lockoutEnd); + + /// + /// Used to record when an attempt to access the user has failed + /// + /// + /// + Task IncrementAccessFailedCount(TUser user); + + /// + /// Used to reset the account access count, typically after the account is successfully accessed + /// + /// + /// + Task ResetAccessFailedCount(TUser user); + + /// + /// Returns the current number of failed access attempts. This number usually will be reset whenever the password is + /// verified or the account is locked out. + /// + /// + /// + Task GetAccessFailedCount(TUser user); + + /// + /// Returns whether the user can be locked out. + /// + /// + /// + Task GetLockoutEnabled(TUser user); + + /// + /// Sets whether the user can be locked out. + /// + /// + /// + /// + Task SetLockoutEnabled(TUser user, bool enabled); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserLoginStore.cs b/src/Microsoft.AspNet.Identity/IUserLoginStore.cs new file mode 100644 index 0000000000..1bb4c1b0f8 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserLoginStore.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface that maps users to login providers, i.e. Google, Facebook, Twitter, Microsoft + /// + /// + /// + public interface IUserLoginStore : IUserStore where TUser : class, IUser + { + /// + /// Adds a user login with the specified provider and key + /// + /// + /// + /// + Task AddLogin(TUser user, UserLoginInfo login); + + /// + /// Removes the user login with the specified combination if it exists, returns true if found and removed + /// + /// + /// + /// + Task RemoveLogin(TUser user, UserLoginInfo login); + + /// + /// Returns the linked accounts for this user + /// + /// + /// + Task> GetLogins(TUser user); + + /// + /// Returns the user associated with this login + /// + /// + Task Find(UserLoginInfo login); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserPasswordStore.cs b/src/Microsoft.AspNet.Identity/IUserPasswordStore.cs new file mode 100644 index 0000000000..ce4bccb8cc --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserPasswordStore.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores a user's password hash + /// + /// + /// + public interface IUserPasswordStore : IUserStore where TUser : class, IUser + { + /// + /// Set the user password hash + /// + /// + /// + /// + Task SetPasswordHash(TUser user, string passwordHash); + + /// + /// Get the user password hash + /// + /// + /// + Task GetPasswordHash(TUser user); + + /// + /// Returns true if a user has a password set + /// + /// + /// + Task HasPassword(TUser user); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserPhoneNumberStore.cs b/src/Microsoft.AspNet.Identity/IUserPhoneNumberStore.cs new file mode 100644 index 0000000000..c46e681cde --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserPhoneNumberStore.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores a user's phoneNumber + /// + /// + /// + public interface IUserPhoneNumberStore : IUserStore where TUser : class, IUser + { + /// + /// Set the user PhoneNumber + /// + /// + /// + /// + Task SetPhoneNumber(TUser user, string phoneNumber); + + /// + /// Get the user phoneNumber + /// + /// + /// + Task GetPhoneNumber(TUser user); + + /// + /// Returns true if the user phone number is confirmed + /// + /// + /// + Task GetPhoneNumberConfirmed(TUser user); + + /// + /// Sets whether the user phone number is confirmed + /// + /// + /// + /// + Task SetPhoneNumberConfirmed(TUser user, bool confirmed); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserRoleStore.cs b/src/Microsoft.AspNet.Identity/IUserRoleStore.cs new file mode 100644 index 0000000000..f2a356f121 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserRoleStore.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface that maps users to their roles + /// + /// + /// + public interface IUserRoleStore : IUserStore where TUser : class, IUser + { + /// + /// Adds a user to role + /// + /// + /// + /// + Task AddToRole(TUser user, string roleName); + + /// + /// Removes the role for the user + /// + /// + /// + /// + Task RemoveFromRole(TUser user, string roleName); + + /// + /// Returns the roles for this user + /// + /// + /// + Task> GetRoles(TUser user); + + /// + /// Returns true if a user is in a role + /// + /// + /// + /// + Task IsInRole(TUser user, string roleName); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserSecurityStampStore.cs b/src/Microsoft.AspNet.Identity/IUserSecurityStampStore.cs new file mode 100644 index 0000000000..5dbdd38f81 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserSecurityStampStore.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores a user's security stamp + /// + /// + /// + public interface IUserSecurityStampStore : IUserStore where TUser : class, IUser + { + /// + /// Set the security stamp for the user + /// + /// + /// + /// + Task SetSecurityStamp(TUser user, string stamp); + + /// + /// Get the user security stamp + /// + /// + /// + Task GetSecurityStamp(TUser user); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserStore.cs b/src/Microsoft.AspNet.Identity/IUserStore.cs new file mode 100644 index 0000000000..1adc85665b --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserStore.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface that exposes basic user management apis + /// + /// + /// + public interface IUserStore : IDisposable where TUser : class, IUser + { + /// + /// Insert a new user + /// + /// + /// + Task Create(TUser user); + + /// + /// Update a user + /// + /// + /// + Task Update(TUser user); + + /// + /// Delete a user + /// + /// + /// + Task Delete(TUser user); + + /// + /// Finds a user + /// + /// + /// + Task FindById(TKey userId); + + /// + /// Find a user by name + /// + /// + /// + Task FindByName(string userName); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs b/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs new file mode 100644 index 0000000000..0a665c2342 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserTokenProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Interface to generate user tokens + /// + public interface IUserTokenProvider where TUser : class, IUser where TKey : IEquatable + { + /// + /// Generate a token for a user + /// + /// + /// + /// + /// + Task Generate(string purpose, UserManager manager, TUser user); + + /// + /// Validate and unprotect a token, returns null if invalid + /// + /// + /// + /// + /// + /// + Task Validate(string purpose, string token, UserManager manager, TUser user); + + /// + /// Notifies the user that a token has been generated, i.e. via email or sms, or can no-op + /// + /// + /// + /// + /// + Task Notify(string token, UserManager manager, TUser user); + + /// + /// Returns true if provider can be used for this user, i.e. could require a user to have an email + /// + /// + /// + /// + Task IsValidProviderForUser(UserManager manager, TUser user); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserTwoFactorStore.cs b/src/Microsoft.AspNet.Identity/IUserTwoFactorStore.cs new file mode 100644 index 0000000000..0ab772403c --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IUserTwoFactorStore.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Stores whether two factor is enabled for a user + /// + /// + /// + public interface IUserTwoFactorStore : IUserStore where TUser : class, IUser + { + /// + /// Sets whether two factor is enabled for the user + /// + /// + /// + /// + Task SetTwoFactorEnabled(TUser user, bool enabled); + + /// + /// Returns whether two factor is enabled for the user + /// + /// + /// + Task GetTwoFactorEnabled(TUser user); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityResult.cs b/src/Microsoft.AspNet.Identity/IdentityResult.cs new file mode 100644 index 0000000000..3ff9bb4a1b --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IdentityResult.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Represents the result of an identity operation + /// + public class IdentityResult + { + private static readonly IdentityResult _success = new IdentityResult(true); + + /// + /// Failure constructor that takes error messages + /// + /// + public IdentityResult(params string[] errors) : this((IEnumerable) errors) + { + } + + /// + /// Failure constructor that takes error messages + /// + /// + public IdentityResult(IEnumerable errors) + { + if (errors == null) + { + errors = new[] {"Resources.DefaultError"}; + } + Succeeded = false; + Errors = errors; + } + + private IdentityResult(bool success) + { + Succeeded = success; + Errors = new string[0]; + } + + /// + /// True if the operation was successful + /// + public bool Succeeded { get; private set; } + + /// + /// List of errors + /// + public IEnumerable Errors { get; private set; } + + /// + /// Static success result + /// + /// + public static IdentityResult Success + { + get { return _success; } + } + + /// + /// Failed helper method + /// + /// + /// + public static IdentityResult Failed(params string[] errors) + { + return new IdentityResult(errors); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PasswordVerificationResult.cs b/src/Microsoft.AspNet.Identity/PasswordVerificationResult.cs new file mode 100644 index 0000000000..93817857d7 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/PasswordVerificationResult.cs @@ -0,0 +1,23 @@ +namespace Microsoft.AspNet.Identity +{ + /// + /// Return result for IPasswordHasher + /// + public enum PasswordVerificationResult + { + /// + /// Password verification failed + /// + Failed = 0, + + /// + /// Success + /// + Success = 1, + + /// + /// Success but should update and rehash the password + /// + SuccessRehashNeeded = 2 + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/Resources.Designer.cs b/src/Microsoft.AspNet.Identity/Resources.Designer.cs new file mode 100644 index 0000000000..24c89d1dd1 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/Resources.Designer.cs @@ -0,0 +1,382 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34011 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Identity { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { +#if NET45 + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Identity.Resources", typeof(Resources).Assembly); +#else + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); +#endif + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An unknown failure has occured.. + /// + internal static string DefaultError { + get { + return ResourceManager.GetString("DefaultError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email '{0}' is already taken.. + /// + internal static string DuplicateEmail { + get { + return ResourceManager.GetString("DuplicateEmail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name {0} is already taken.. + /// + internal static string DuplicateName { + get { + return ResourceManager.GetString("DuplicateName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user with that external login already exists.. + /// + internal static string ExternalLoginExists { + get { + return ResourceManager.GetString("ExternalLoginExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email '{0}' is invalid.. + /// + internal static string InvalidEmail { + get { + return ResourceManager.GetString("InvalidEmail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid token.. + /// + internal static string InvalidToken { + get { + return ResourceManager.GetString("InvalidToken", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User name {0} is invalid, can only contain letters or digits.. + /// + internal static string InvalidUserName { + get { + return ResourceManager.GetString("InvalidUserName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lockout is not enabled for this user.. + /// + internal static string LockoutNotEnabled { + get { + return ResourceManager.GetString("LockoutNotEnabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No IUserTokenProvider is registered.. + /// + internal static string NoTokenProvider { + get { + return ResourceManager.GetString("NoTokenProvider", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No IUserTwoFactorProvider for '{0}' is registered.. + /// + internal static string NoTwoFactorProvider { + get { + return ResourceManager.GetString("NoTwoFactorProvider", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Incorrect password.. + /// + internal static string PasswordMismatch { + get { + return ResourceManager.GetString("PasswordMismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passwords must have at least one digit ('0'-'9').. + /// + internal static string PasswordRequireDigit { + get { + return ResourceManager.GetString("PasswordRequireDigit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passwords must have at least one lowercase ('a'-'z').. + /// + internal static string PasswordRequireLower { + get { + return ResourceManager.GetString("PasswordRequireLower", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passwords must have at least one non letter or digit character.. + /// + internal static string PasswordRequireNonLetterOrDigit { + get { + return ResourceManager.GetString("PasswordRequireNonLetterOrDigit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passwords must have at least one uppercase ('A'-'Z').. + /// + internal static string PasswordRequireUpper { + get { + return ResourceManager.GetString("PasswordRequireUpper", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passwords must be at least {0} characters.. + /// + internal static string PasswordTooShort { + get { + return ResourceManager.GetString("PasswordTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} cannot be null or empty.. + /// + internal static string PropertyTooShort { + get { + return ResourceManager.GetString("PropertyTooShort", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Role {0} does not exist.. + /// + internal static string RoleNotFound { + get { + return ResourceManager.GetString("RoleNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IQueryableRoleStore<TRole>.. + /// + internal static string StoreNotIQueryableRoleStore { + get { + return ResourceManager.GetString("StoreNotIQueryableRoleStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IQueryableUserStore<TUser>.. + /// + internal static string StoreNotIQueryableUserStore { + get { + return ResourceManager.GetString("StoreNotIQueryableUserStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserClaimStore<TUser>.. + /// + internal static string StoreNotIUserClaimStore { + get { + return ResourceManager.GetString("StoreNotIUserClaimStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserConfirmationStore<TUser>.. + /// + internal static string StoreNotIUserConfirmationStore { + get { + return ResourceManager.GetString("StoreNotIUserConfirmationStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserEmailStore<TUser>.. + /// + internal static string StoreNotIUserEmailStore { + get { + return ResourceManager.GetString("StoreNotIUserEmailStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserLockoutStore<TUser>.. + /// + internal static string StoreNotIUserLockoutStore { + get { + return ResourceManager.GetString("StoreNotIUserLockoutStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserLoginStore<TUser>.. + /// + internal static string StoreNotIUserLoginStore { + get { + return ResourceManager.GetString("StoreNotIUserLoginStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserPasswordStore<TUser>.. + /// + internal static string StoreNotIUserPasswordStore { + get { + return ResourceManager.GetString("StoreNotIUserPasswordStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserPhoneNumberStore<TUser>.. + /// + internal static string StoreNotIUserPhoneNumberStore { + get { + return ResourceManager.GetString("StoreNotIUserPhoneNumberStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserRoleStore<TUser>.. + /// + internal static string StoreNotIUserRoleStore { + get { + return ResourceManager.GetString("StoreNotIUserRoleStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserSecurityStampStore<TUser>.. + /// + internal static string StoreNotIUserSecurityStampStore { + get { + return ResourceManager.GetString("StoreNotIUserSecurityStampStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Store does not implement IUserTwoFactorStore<TUser>.. + /// + internal static string StoreNotIUserTwoFactorStore { + get { + return ResourceManager.GetString("StoreNotIUserTwoFactorStore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User already has a password set.. + /// + internal static string UserAlreadyHasPassword { + get { + return ResourceManager.GetString("UserAlreadyHasPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User already in role.. + /// + internal static string UserAlreadyInRole { + get { + return ResourceManager.GetString("UserAlreadyInRole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UserId not found.. + /// + internal static string UserIdNotFound { + get { + return ResourceManager.GetString("UserIdNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User {0} does not exist.. + /// + internal static string UserNameNotFound { + get { + return ResourceManager.GetString("UserNameNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User is not in role.. + /// + internal static string UserNotInRole { + get { + return ResourceManager.GetString("UserNotInRole", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Identity/Resources.resx b/src/Microsoft.AspNet.Identity/Resources.resx new file mode 100644 index 0000000000..c9b40f983d --- /dev/null +++ b/src/Microsoft.AspNet.Identity/Resources.resx @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An unknown failure has occured. + Default identity result error message + + + Email '{0}' is already taken. + error for duplicate emails + + + Name {0} is already taken. + error for duplicate usernames + + + A user with that external login already exists. + Error when a login already linked + + + Email '{0}' is invalid. + invalid email + + + Invalid token. + Error when a token is not recognized + + + User name {0} is invalid, can only contain letters or digits. + usernames can only contain letters or digits + + + Lockout is not enabled for this user. + error when lockout is not enabled + + + No IUserTokenProvider is registered. + Error when there is no IUserTokenProvider + + + No IUserTwoFactorProvider for '{0}' is registered. + Error when there is no provider found + + + Incorrect password. + Error when a password doesn't match + + + Passwords must have at least one digit ('0'-'9'). + Error when passwords do not have a digit + + + Passwords must have at least one lowercase ('a'-'z'). + Error when passwords do not have a lowercase letter + + + Passwords must have at least one non letter or digit character. + Error when password does not have enough letter or digit characters + + + Passwords must have at least one uppercase ('A'-'Z'). + Error when passwords do not have an uppercase letter + + + Passwords must be at least {0} characters. + Error message for passwords that are too short + + + {0} cannot be null or empty. + error for empty or null usernames + + + Role {0} does not exist. + error when a role does not exist + + + Store does not implement IQueryableRoleStore<TRole>. + error when the store does not implement this interface + + + Store does not implement IQueryableUserStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserClaimStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserConfirmationStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserEmailStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserLockoutStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserLoginStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserPasswordStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserPhoneNumberStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserRoleStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserSecurityStampStore<TUser>. + error when the store does not implement this interface + + + Store does not implement IUserTwoFactorStore<TUser>. + error when the store does not implement this interface + + + User already has a password set. + error when AddPassword called when a user already has a password + + + User already in role. + Error when a user is already in a role + + + UserId not found. + No user with this id found + + + User {0} does not exist. + error when a user does not exist + + + User is not in role. + Error when a user is not in the role + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs b/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs new file mode 100644 index 0000000000..d6facc691f --- /dev/null +++ b/src/Microsoft.AspNet.Identity/Rfc6238AuthenticationService.cs @@ -0,0 +1,115 @@ +#if NET45 + +using System; +using System.Diagnostics; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.AspNet.Identity +{ + internal sealed class SecurityToken + { + private readonly byte[] _data; + + public SecurityToken(byte[] data) + { + _data = (byte[]) data.Clone(); + } + + internal byte[] GetDataNoClone() + { + return _data; + } + } + + internal static class Rfc6238AuthenticationService + { + private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3); + private static readonly Encoding _encoding = new UTF8Encoding(false, true); + + private static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier) + { + // # of 0's = length of pin + const int mod = 1000000; + + // See https://tools.ietf.org/html/rfc4226 + // We can add an optional modifier + var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long) timestepNumber)); + var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier)); + + // Generate DT string + var offset = hash[hash.Length - 1] & 0xf; + Debug.Assert(offset + 4 < hash.Length); + var binaryCode = (hash[offset] & 0x7f) << 24 + | (hash[offset + 1] & 0xff) << 16 + | (hash[offset + 2] & 0xff) << 8 + | (hash[offset + 3] & 0xff); + + return binaryCode%mod; + } + + private static byte[] ApplyModifier(byte[] input, string modifier) + { + if (String.IsNullOrEmpty(modifier)) + { + return input; + } + + var modifierBytes = _encoding.GetBytes(modifier); + var combined = new byte[checked(input.Length + modifierBytes.Length)]; + Buffer.BlockCopy(input, 0, combined, 0, input.Length); + Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length); + return combined; + } + + // More info: https://tools.ietf.org/html/rfc6238#section-4 + private static ulong GetCurrentTimeStepNumber() + { + var delta = DateTime.UtcNow - _unixEpoch; + return (ulong) (delta.Ticks/_timestep.Ticks); + } + + public static int GenerateCode(SecurityToken securityToken, string modifier = null) + { + if (securityToken == null) + { + throw new ArgumentNullException("securityToken"); + } + + // Allow a variance of no greater than 90 seconds in either direction + var currentTimeStep = GetCurrentTimeStepNumber(); + using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone())) + { + return ComputeTotp(hashAlgorithm, currentTimeStep, modifier); + } + } + + public static bool ValidateCode(SecurityToken securityToken, int code, string modifier = null) + { + if (securityToken == null) + { + throw new ArgumentNullException("securityToken"); + } + + // Allow a variance of no greater than 90 seconds in either direction + var currentTimeStep = GetCurrentTimeStepNumber(); + using (var hashAlgorithm = new HMACSHA1(securityToken.GetDataNoClone())) + { + for (var i = -2; i <= 2; i++) + { + var computedTotp = ComputeTotp(hashAlgorithm, (ulong) ((long) currentTimeStep + i), modifier); + if (computedTotp == code) + { + return true; + } + } + } + + // No match + return false; + } + } +} +#endif diff --git a/src/Microsoft.AspNet.Identity/UserLoginInfo.cs b/src/Microsoft.AspNet.Identity/UserLoginInfo.cs new file mode 100644 index 0000000000..79a6f7f46a --- /dev/null +++ b/src/Microsoft.AspNet.Identity/UserLoginInfo.cs @@ -0,0 +1,29 @@ +namespace Microsoft.AspNet.Identity +{ + /// + /// Represents a linked login for a user (i.e. a local username/password or a facebook/google account + /// + public sealed class UserLoginInfo + { + /// + /// Constructor + /// + /// + /// + public UserLoginInfo(string loginProvider, string providerKey) + { + LoginProvider = loginProvider; + ProviderKey = providerKey; + } + + /// + /// Provider for the linked login, i.e. Local, Facebook, Google, etc. + /// + public string LoginProvider { get; set; } + + /// + /// Key for the linked login at the provider + /// + public string ProviderKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs new file mode 100644 index 0000000000..6238936636 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -0,0 +1,1711 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +#if NET45 +using System.Security.Claims; +#endif +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Exposes user related api which will automatically save changes to the UserStore + /// + /// + /// + public class UserManager : IDisposable + where TUser : class, IUser + where TKey : IEquatable + { + private readonly Dictionary> _factors = + new Dictionary>(); + + private IClaimsIdentityFactory _claimsFactory; + private TimeSpan _defaultLockout = TimeSpan.Zero; + private bool _disposed; + private IPasswordHasher _passwordHasher; + private IIdentityValidator _passwordValidator; + private IIdentityValidator _userValidator; + + /// + /// Constructor + /// + /// The IUserStore is responsible for commiting changes via the UpdateAsync/CreateAsync methods + public UserManager(IUserStore store) + { + if (store == null) + { + throw new ArgumentNullException("store"); + } + Store = store; + //UserValidator = new UserValidator(this); + //PasswordValidator = new MinimumLengthValidator(6); + //PasswordHasher = new PasswordHasher(); + //ClaimsIdentityFactory = new ClaimsIdentityFactory(); + } + + /// + /// Persistence abstraction that the Manager operates against + /// + protected internal IUserStore Store { get; set; } + + /// + /// Used to hash/verify passwords + /// + public IPasswordHasher PasswordHasher + { + get + { + ThrowIfDisposed(); + return _passwordHasher; + } + set + { + ThrowIfDisposed(); + if (value == null) + { + throw new ArgumentNullException("value"); + } + _passwordHasher = value; + } + } + + /// + /// Used to validate users before persisting changes + /// + public IIdentityValidator UserValidator + { + get + { + ThrowIfDisposed(); + return _userValidator; + } + set + { + ThrowIfDisposed(); + if (value == null) + { + throw new ArgumentNullException("value"); + } + _userValidator = value; + } + } + + /// + /// Used to validate passwords before persisting changes + /// + public IIdentityValidator PasswordValidator + { + get + { + ThrowIfDisposed(); + return _passwordValidator; + } + set + { + ThrowIfDisposed(); + if (value == null) + { + throw new ArgumentNullException("value"); + } + _passwordValidator = value; + } + } + + /// + /// Used to create claims identities from users + /// + public IClaimsIdentityFactory ClaimsIdentityFactory + { + get + { + ThrowIfDisposed(); + return _claimsFactory; + } + set + { + ThrowIfDisposed(); + if (value == null) + { + throw new ArgumentNullException("value"); + } + _claimsFactory = value; + } + } + + /// + /// Used to send email + /// + public IIdentityMessageService EmailService { get; set; } + + /// + /// Used to send a sms message + /// + public IIdentityMessageService SmsService { get; set; } + + /// + /// Used for generating ResetPassword and Confirmation Tokens + /// + public IUserTokenProvider UserTokenProvider { get; set; } + + /// + /// If true, will enable user lockout when users are created + /// + public bool UserLockoutEnabledByDefault { get; set; } + + /// + /// Number of access attempts allowed for a user before lockout (if enabled) + /// + public int MaxFailedAccessAttemptsBeforeLockout { get; set; } + + /// + /// Default amount of time an user is locked out for after MaxFailedAccessAttempsBeforeLockout is reached + /// + public TimeSpan DefaultAccountLockoutTimeSpan + { + get { return _defaultLockout; } + set { _defaultLockout = value; } + } + + /// + /// Returns true if the store is an IUserTwoFactorStore + /// + public virtual bool SupportsUserTwoFactor + { + get + { + ThrowIfDisposed(); + return Store is IUserTwoFactorStore; + } + } + + /// + /// Returns true if the store is an IUserPasswordStore + /// + public virtual bool SupportsUserPassword + { + get + { + ThrowIfDisposed(); + return Store is IUserPasswordStore; + } + } + + /// + /// Returns true if the store is an IUserSecurityStore + /// + public virtual bool SupportsUserSecurityStamp + { + get + { + ThrowIfDisposed(); + return Store is IUserSecurityStampStore; + } + } + + /// + /// Returns true if the store is an IUserRoleStore + /// + public virtual bool SupportsUserRole + { + get + { + ThrowIfDisposed(); + return Store is IUserRoleStore; + } + } + + /// + /// Returns true if the store is an IUserLoginStore + /// + public virtual bool SupportsUserLogin + { + get + { + ThrowIfDisposed(); + return Store is IUserLoginStore; + } + } + + /// + /// Returns true if the store is an IUserEmailStore + /// + public virtual bool SupportsUserEmail + { + get + { + ThrowIfDisposed(); + return Store is IUserEmailStore; + } + } + + /// + /// Returns true if the store is an IUserPhoneNumberStore + /// + public virtual bool SupportsUserPhoneNumber + { + get + { + ThrowIfDisposed(); + return Store is IUserPhoneNumberStore; + } + } + + /// + /// Returns true if the store is an IUserClaimStore + /// + public virtual bool SupportsUserClaim + { + get + { + ThrowIfDisposed(); + return false; + //return Store is IUserClaimStore; + } + } + + /// + /// Returns true if the store is an IUserLockoutStore + /// + public virtual bool SupportsUserLockout + { + get + { + ThrowIfDisposed(); + return Store is IUserLockoutStore; + } + } + + /// + /// Returns true if the store is an IQueryableUserStore + /// + public virtual bool SupportsQueryableUsers + { + get + { + ThrowIfDisposed(); + return Store is IQueryableUserStore; + } + } + + + /// + /// Returns an IQueryable of users if the store is an IQueryableUserStore + /// + public virtual IQueryable Users + { + get + { + var queryableStore = Store as IQueryableUserStore; + if (queryableStore == null) + { + throw new NotSupportedException(Resources.StoreNotIQueryableUserStore); + } + return queryableStore.Users; + } + } + + /// + /// Dictionary mapping user two factor providers + /// + public IDictionary> TwoFactorProviders + { + get { return _factors; } + } + + /// + /// Dispose the store context + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + +#if NET45 + /// + /// Creates a ClaimsIdentity representing the user + /// + /// + /// + /// + public virtual Task CreateIdentity(TUser user, string authenticationType) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + return ClaimsIdentityFactory.Create(this, user, authenticationType); + } +#endif + + /// + /// Create a user with no password + /// + /// + /// + public virtual async Task Create(TUser user) + { + ThrowIfDisposed(); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + var result = await UserValidator.Validate(user).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + if (UserLockoutEnabledByDefault && SupportsUserLockout) + { + await GetUserLockoutStore().SetLockoutEnabled(user, true).ConfigureAwait(false); + } + await Store.Create(user).ConfigureAwait(false); + return IdentityResult.Success; + } + + /// + /// Update a user + /// + /// + /// + public virtual async Task Update(TUser user) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + + var result = await UserValidator.Validate(user).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + await Store.Update(user).ConfigureAwait(false); + return IdentityResult.Success; + } + + /// + /// Delete a user + /// + /// + /// + public virtual async Task Delete(TUser user) + { + ThrowIfDisposed(); + await Store.Delete(user).ConfigureAwait(false); + return IdentityResult.Success; + } + + /// + /// Find a user by id + /// + /// + /// + public virtual Task FindById(TKey userId) + { + ThrowIfDisposed(); + return Store.FindById(userId); + } + + /// + /// Find a user by name + /// + /// + /// + public virtual Task FindByName(string userName) + { + ThrowIfDisposed(); + if (userName == null) + { + throw new ArgumentNullException("userName"); + } + return Store.FindByName(userName); + } + + // IUserPasswordStore methods + private IUserPasswordStore GetPasswordStore() + { + var cast = Store as IUserPasswordStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserPasswordStore); + } + return cast; + } + + /// + /// Create a user and associates it with the given password (if one is provided) + /// + /// + /// + /// + public virtual async Task Create(TUser user, string password) + { + ThrowIfDisposed(); + var passwordStore = GetPasswordStore(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + if (password == null) + { + throw new ArgumentNullException("password"); + } + var result = await UpdatePasswordInternal(passwordStore, user, password).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + return await Create(user).ConfigureAwait(false); + } + + /// + /// Return a user with the specified username and password or null if there is no match. + /// + /// + /// + /// + public virtual async Task Find(string userName, string password) + { + ThrowIfDisposed(); + var user = await FindByName(userName).ConfigureAwait(false); + if (user == null) + { + return null; + } + return await CheckPassword(user, password).ConfigureAwait(false) ? user : null; + } + + /// + /// Returns true if the password combination is valid for the user + /// + /// + /// + /// + public virtual async Task CheckPassword(TUser user, string password) + { + ThrowIfDisposed(); + var passwordStore = GetPasswordStore(); + if (user == null) + { + return false; + } + return await VerifyPassword(passwordStore, user, password).ConfigureAwait(false); + } + + /// + /// Returns true if the user has a password + /// + /// + /// + public virtual async Task HasPassword(TKey userId) + { + ThrowIfDisposed(); + var passwordStore = GetPasswordStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await passwordStore.HasPassword(user).ConfigureAwait(false); + } + + /// + /// Add a user password only if one does not already exist + /// + /// + /// + /// + public virtual async Task AddPassword(TKey userId, string password) + { + ThrowIfDisposed(); + var passwordStore = GetPasswordStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + var hash = await passwordStore.GetPasswordHash(user).ConfigureAwait(false); + if (hash != null) + { + return new IdentityResult(Resources.UserAlreadyHasPassword); + } + var result = await UpdatePasswordInternal(passwordStore, user, password).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + return await Update(user).ConfigureAwait(false); + } + + /// + /// Change a user password + /// + /// + /// + /// + /// + public virtual async Task ChangePassword(TKey userId, string currentPassword, + string newPassword) + { + ThrowIfDisposed(); + var passwordStore = GetPasswordStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (await VerifyPassword(passwordStore, user, currentPassword).ConfigureAwait(false)) + { + var result = await UpdatePasswordInternal(passwordStore, user, newPassword).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + return await Update(user).ConfigureAwait(false); + } + return IdentityResult.Failed(Resources.PasswordMismatch); + } + + /// + /// Remove a user's password + /// + /// + /// + public virtual async Task RemovePassword(TKey userId) + { + ThrowIfDisposed(); + var passwordStore = GetPasswordStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await passwordStore.SetPasswordHash(user, null).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + internal async Task UpdatePasswordInternal(IUserPasswordStore passwordStore, + TUser user, string newPassword) + { + var result = await PasswordValidator.Validate(newPassword).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + await + passwordStore.SetPasswordHash(user, PasswordHasher.HashPassword(newPassword)).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return IdentityResult.Success; + } + + /// + /// By default, retrieves the hashed password from the user store and calls PasswordHasher.VerifyHashPassword + /// + /// + /// + /// + /// + protected virtual async Task VerifyPassword(IUserPasswordStore store, TUser user, + string password) + { + var hash = await store.GetPasswordHash(user).ConfigureAwait(false); + return PasswordHasher.VerifyHashedPassword(hash, password) != PasswordVerificationResult.Failed; + } + + // IUserSecurityStampStore methods + private IUserSecurityStampStore GetSecurityStore() + { + var cast = Store as IUserSecurityStampStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserSecurityStampStore); + } + return cast; + } + + /// + /// Returns the current security stamp for a user + /// + /// + /// + public virtual async Task GetSecurityStamp(TKey userId) + { + ThrowIfDisposed(); + var securityStore = GetSecurityStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await securityStore.GetSecurityStamp(user).ConfigureAwait(false); + } + + /// + /// Generate a new security stamp for a user, used for SignOutEverywhere functionality + /// + /// + /// + public virtual async Task UpdateSecurityStamp(TKey userId) + { + ThrowIfDisposed(); + var securityStore = GetSecurityStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await securityStore.SetSecurityStamp(user, NewSecurityStamp()).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Generate a password reset token for the user using the UserTokenProvider + /// + /// + /// + public virtual async Task GeneratePasswordResetToken(TKey userId) + { + ThrowIfDisposed(); + return await GenerateUserToken("ResetPassword", userId); + } + + /// + /// Reset a user's password using a reset password token + /// + /// + /// + /// + /// + public virtual async Task ResetPassword(TKey userId, string token, string newPassword) + { + ThrowIfDisposed(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + // Make sure the token is valid and the stamp matches + if (!await VerifyUserToken(userId, "ResetPassword", token).ConfigureAwait(false)) + { + return IdentityResult.Failed(Resources.InvalidToken); + } + var passwordStore = GetPasswordStore(); + var result = await UpdatePasswordInternal(passwordStore, user, newPassword).ConfigureAwait(false); + if (!result.Succeeded) + { + return result; + } + return await Update(user).ConfigureAwait(false); + } + + // Update the security stamp if the store supports it + internal async Task UpdateSecurityStampInternal(TUser user) + { + if (SupportsUserSecurityStamp) + { + await GetSecurityStore().SetSecurityStamp(user, NewSecurityStamp()).ConfigureAwait(false); + } + } + + private static string NewSecurityStamp() + { + return Guid.NewGuid().ToString(); + } + + // IUserLoginStore methods + private IUserLoginStore GetLoginStore() + { + var cast = Store as IUserLoginStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserLoginStore); + } + return cast; + } + + /// + /// Returns the user associated with this login + /// + /// + public virtual Task Find(UserLoginInfo login) + { + ThrowIfDisposed(); + return GetLoginStore().Find(login); + } + + /// + /// Remove a user login + /// + /// + /// + /// + public virtual async Task RemoveLogin(TKey userId, UserLoginInfo login) + { + ThrowIfDisposed(); + var loginStore = GetLoginStore(); + if (login == null) + { + throw new ArgumentNullException("login"); + } + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await loginStore.RemoveLogin(user, login).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Associate a login with a user + /// + /// + /// + /// + public virtual async Task AddLogin(TKey userId, UserLoginInfo login) + { + ThrowIfDisposed(); + var loginStore = GetLoginStore(); + if (login == null) + { + throw new ArgumentNullException("login"); + } + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + var existingUser = await Find(login).ConfigureAwait(false); + if (existingUser != null) + { + return IdentityResult.Failed(Resources.ExternalLoginExists); + } + await loginStore.AddLogin(user, login).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Gets the logins for a user. + /// + /// + /// + public virtual async Task> GetLogins(TKey userId) + { + ThrowIfDisposed(); + var loginStore = GetLoginStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await loginStore.GetLogins(user).ConfigureAwait(false); + } + +#if NET45 + // IUserClaimStore methods + private IUserClaimStore GetClaimStore() + { + var cast = Store as IUserClaimStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserClaimStore); + } + return cast; + } + + /// + /// Add a user claim + /// + /// + /// + /// + public virtual async Task AddClaim(TKey userId, Claim claim) + { + ThrowIfDisposed(); + var claimStore = GetClaimStore(); + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await claimStore.AddClaim(user, claim).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Remove a user claim + /// + /// + /// + /// + public virtual async Task RemoveClaim(TKey userId, Claim claim) + { + ThrowIfDisposed(); + var claimStore = GetClaimStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await claimStore.RemoveClaim(user, claim).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Get a users's claims + /// + /// + /// + public virtual async Task> GetClaims(TKey userId) + { + ThrowIfDisposed(); + var claimStore = GetClaimStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await claimStore.GetClaims(user).ConfigureAwait(false); + } +#endif + + private IUserRoleStore GetUserRoleStore() + { + var cast = Store as IUserRoleStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserRoleStore); + } + return cast; + } + + /// + /// Add a user to a role + /// + /// + /// + /// + public virtual async Task AddToRole(TKey userId, string role) + { + ThrowIfDisposed(); + var userRoleStore = GetUserRoleStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + var userRoles = await userRoleStore.GetRoles(user).ConfigureAwait(false); + if (userRoles.Contains(role)) + { + return new IdentityResult(Resources.UserAlreadyInRole); + } + await userRoleStore.AddToRole(user, role).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Remove a user from a role. + /// + /// + /// + /// + public virtual async Task RemoveFromRole(TKey userId, string role) + { + ThrowIfDisposed(); + var userRoleStore = GetUserRoleStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (!await userRoleStore.IsInRole(user, role).ConfigureAwait(false)) + { + return new IdentityResult(Resources.UserNotInRole); + } + await userRoleStore.RemoveFromRole(user, role).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Returns the roles for the user + /// + /// + /// + public virtual async Task> GetRoles(TKey userId) + { + ThrowIfDisposed(); + var userRoleStore = GetUserRoleStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await userRoleStore.GetRoles(user).ConfigureAwait(false); + } + + /// + /// Returns true if the user is in the specified role + /// + /// + /// + /// + public virtual async Task IsInRole(TKey userId, string role) + { + ThrowIfDisposed(); + var userRoleStore = GetUserRoleStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await userRoleStore.IsInRole(user, role).ConfigureAwait(false); + } + + // IUserEmailStore methods + internal IUserEmailStore GetEmailStore() + { + var cast = Store as IUserEmailStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserEmailStore); + } + return cast; + } + + /// + /// Get a user's email + /// + /// + /// + public virtual async Task GetEmail(TKey userId) + { + ThrowIfDisposed(); + var store = GetEmailStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetEmail(user).ConfigureAwait(false); + } + + /// + /// Set a user's email + /// + /// + /// + /// + public virtual async Task SetEmail(TKey userId, string email) + { + ThrowIfDisposed(); + var store = GetEmailStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await store.SetEmail(user, email).ConfigureAwait(false); + await store.SetEmailConfirmed(user, false).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Find a user by his email + /// + /// + /// + public virtual Task FindByEmail(string email) + { + ThrowIfDisposed(); + var store = GetEmailStore(); + if (email == null) + { + throw new ArgumentNullException("email"); + } + return store.FindByEmail(email); + } + + /// + /// Get the confirmation token for the user + /// + /// + /// + public virtual Task GenerateEmailConfirmationToken(TKey userId) + { + ThrowIfDisposed(); + return GenerateUserToken("Confirmation", userId); + } + + /// + /// Confirm the user with confirmation token + /// + /// + /// + /// + public virtual async Task ConfirmEmail(TKey userId, string token) + { + ThrowIfDisposed(); + var store = GetEmailStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (!await VerifyUserToken(userId, "Confirmation", token)) + { + return IdentityResult.Failed(Resources.InvalidToken); + } + await store.SetEmailConfirmed(user, true).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Returns true if the user's email has been confirmed + /// + /// + /// + public virtual async Task IsEmailConfirmed(TKey userId) + { + ThrowIfDisposed(); + var store = GetEmailStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetEmailConfirmed(user).ConfigureAwait(false); + } + + // IUserPhoneNumberStore methods + internal IUserPhoneNumberStore GetPhoneNumberStore() + { + var cast = Store as IUserPhoneNumberStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserPhoneNumberStore); + } + return cast; + } + + /// + /// Get a user's phoneNumber + /// + /// + /// + public virtual async Task GetPhoneNumber(TKey userId) + { + ThrowIfDisposed(); + var store = GetPhoneNumberStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetPhoneNumber(user).ConfigureAwait(false); + } + + /// + /// Set a user's phoneNumber + /// + /// + /// + /// + public virtual async Task SetPhoneNumber(TKey userId, string phoneNumber) + { + ThrowIfDisposed(); + var store = GetPhoneNumberStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await store.SetPhoneNumber(user, phoneNumber).ConfigureAwait(false); + await store.SetPhoneNumberConfirmed(user, false).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Set a user's phoneNumber with the verification token + /// + /// + /// + /// + /// + public virtual async Task ChangePhoneNumber(TKey userId, string phoneNumber, string token) + { + ThrowIfDisposed(); + var store = GetPhoneNumberStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (await VerifyChangePhoneNumberToken(userId, token, phoneNumber).ConfigureAwait(false)) + { + await store.SetPhoneNumber(user, phoneNumber).ConfigureAwait(false); + await store.SetPhoneNumberConfirmed(user, true).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + return IdentityResult.Failed(Resources.InvalidToken); + } + + /// + /// Returns true if the user's phone number has been confirmed + /// + /// + /// + public virtual async Task IsPhoneNumberConfirmed(TKey userId) + { + ThrowIfDisposed(); + var store = GetPhoneNumberStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetPhoneNumberConfirmed(user).ConfigureAwait(false); + } + + // Two factor APIS + +#if NET45 + internal async Task CreateSecurityToken(TKey userId) + { + return + new SecurityToken(Encoding.Unicode.GetBytes(await GetSecurityStamp(userId).ConfigureAwait(false))); + } + + /// + /// Get a phone number code for a user and phone number + /// + /// + /// + /// + public virtual async Task GenerateChangePhoneNumberToken(TKey userId, string phoneNumber) + { + ThrowIfDisposed(); + return + Rfc6238AuthenticationService.GenerateCode(await CreateSecurityToken(userId), phoneNumber) + .ToString(CultureInfo.InvariantCulture); + } +#endif + + /// + /// Verify a phone number code for a specific user and phone number + /// + /// + /// + /// + /// + public virtual async Task VerifyChangePhoneNumberToken(TKey userId, string token, string phoneNumber) + { + ThrowIfDisposed(); +#if NET45 + var securityToken = await CreateSecurityToken(userId); + int code; + if (securityToken != null && Int32.TryParse(token, out code)) + { + return Rfc6238AuthenticationService.ValidateCode(securityToken, code, phoneNumber); + } +#endif + return false; + } + + /// + /// Verify a user token with the specified purpose + /// + /// + /// + /// + /// + public virtual async Task VerifyUserToken(TKey userId, string purpose, string token) + { + ThrowIfDisposed(); + if (UserTokenProvider == null) + { + throw new NotSupportedException(Resources.NoTokenProvider); + } + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + // Make sure the token is valid + return await UserTokenProvider.Validate(purpose, token, this, user).ConfigureAwait(false); + } + + /// + /// Get a user token for a specific purpose + /// + /// + /// + /// + public virtual async Task GenerateUserToken(string purpose, TKey userId) + { + ThrowIfDisposed(); + if (UserTokenProvider == null) + { + throw new NotSupportedException(Resources.NoTokenProvider); + } + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await UserTokenProvider.Generate(purpose, this, user).ConfigureAwait(false); + } + + /// + /// Register a user two factor provider + /// + /// + /// + public virtual void RegisterTwoFactorProvider(string twoFactorProvider, IUserTokenProvider provider) + { + ThrowIfDisposed(); + if (twoFactorProvider == null) + { + throw new ArgumentNullException("twoFactorProvider"); + } + if (provider == null) + { + throw new ArgumentNullException("provider"); + } + TwoFactorProviders[twoFactorProvider] = provider; + } + + /// + /// Returns a list of valid two factor providers for a user + /// + /// + /// + public virtual async Task> GetValidTwoFactorProviders(TKey userId) + { + ThrowIfDisposed(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + var results = new List(); + foreach (var f in TwoFactorProviders) + { + if (await f.Value.IsValidProviderForUser(this, user)) + { + results.Add(f.Key); + } + } + return results; + } + + /// + /// Verify a user token with the specified provider + /// + /// + /// + /// + /// + public virtual async Task VerifyTwoFactorToken(TKey userId, string twoFactorProvider, string token) + { + ThrowIfDisposed(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (!_factors.ContainsKey(twoFactorProvider)) + { + throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, Resources.NoTwoFactorProvider, + twoFactorProvider)); + } + // Make sure the token is valid + var provider = _factors[twoFactorProvider]; + return await provider.Validate(twoFactorProvider, token, this, user).ConfigureAwait(false); + } + + /// + /// Get a user token for a specific user factor provider + /// + /// + /// + /// + public virtual async Task GenerateTwoFactorToken(TKey userId, string twoFactorProvider) + { + ThrowIfDisposed(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (!_factors.ContainsKey(twoFactorProvider)) + { + throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, Resources.NoTwoFactorProvider, + twoFactorProvider)); + } + return await _factors[twoFactorProvider].Generate(twoFactorProvider, this, user).ConfigureAwait(false); + } + + /// + /// Notify a user with a token from a specific user factor provider + /// + /// + /// + /// + /// + public virtual async Task NotifyTwoFactorToken(TKey userId, string twoFactorProvider, + string token) + { + ThrowIfDisposed(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (!_factors.ContainsKey(twoFactorProvider)) + { + throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, Resources.NoTwoFactorProvider, + twoFactorProvider)); + } + await _factors[twoFactorProvider].Notify(token, this, user).ConfigureAwait(false); + return IdentityResult.Success; + } + + // IUserFactorStore methods + internal IUserTwoFactorStore GetUserTwoFactorStore() + { + var cast = Store as IUserTwoFactorStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserTwoFactorStore); + } + return cast; + } + + /// + /// Get a user's two factor provider + /// + /// + /// + public virtual async Task GetTwoFactorEnabled(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserTwoFactorStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetTwoFactorEnabled(user).ConfigureAwait(false); + } + + /// + /// Set whether a user has two factor enabled or not + /// + /// + /// + /// + public virtual async Task SetTwoFactorEnabled(TKey userId, bool enabled) + { + ThrowIfDisposed(); + var store = GetUserTwoFactorStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await store.SetTwoFactorEnabled(user, enabled).ConfigureAwait(false); + await UpdateSecurityStampInternal(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + // SMS/Email methods + + /// + /// Send an email to the user + /// + /// + /// + /// + /// + public virtual async Task SendEmail(TKey userId, string subject, string body) + { + ThrowIfDisposed(); + if (EmailService != null) + { + var msg = new IdentityMessage + { + Destination = await GetEmail(userId), + Subject = subject, + Body = body, + }; + await EmailService.Send(msg); + } + } + + /// + /// Send a user a sms message + /// + /// + /// + /// + public virtual async Task SendSms(TKey userId, string message) + { + ThrowIfDisposed(); + if (SmsService != null) + { + var msg = new IdentityMessage + { + Destination = await GetPhoneNumber(userId), + Body = message + }; + await SmsService.Send(msg); + } + } + + // IUserLockoutStore methods + internal IUserLockoutStore GetUserLockoutStore() + { + var cast = Store as IUserLockoutStore; + if (cast == null) + { + throw new NotSupportedException(Resources.StoreNotIUserLockoutStore); + } + return cast; + } + + /// + /// Returns true if the user is locked out + /// + /// + /// + public virtual async Task IsLockedOut(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (await store.GetLockoutEnabled(user).ConfigureAwait(false)) + { + var lockoutTime = await store.GetLockoutEndDate(user).ConfigureAwait((false)); + return lockoutTime >= DateTimeOffset.UtcNow; + } + return false; + } + + /// + /// Sets whether the user allows lockout + /// + /// + /// + /// + public virtual async Task SetLockoutEnabled(TKey userId, bool enabled) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await store.SetLockoutEnabled(user, enabled).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Returns whether the user allows lockout + /// + /// + /// + public virtual async Task GetLockoutEnabled(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetLockoutEnabled(user).ConfigureAwait(false); + } + + /// + /// Returns the user lockout end date + /// + /// + /// + public virtual async Task GetLockoutEndDate(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetLockoutEndDate(user).ConfigureAwait(false); + } + + /// + /// Sets the user lockout end date + /// + /// + /// + /// + public virtual async Task SetLockoutEndDate(TKey userId, DateTimeOffset lockoutEnd) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + if (!await store.GetLockoutEnabled(user).ConfigureAwait((false))) + { + return IdentityResult.Failed(Resources.LockoutNotEnabled); + } + await store.SetLockoutEndDate(user, lockoutEnd).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Increments the access failed count for the user and if the failed access account is greater than or equal + /// to the MaxFailedAccessAttempsBeforeLockout, the user will be locked out for the next DefaultAccountLockoutTimeSpan + /// and the AccessFailedCount will be reset to 0. + /// + /// + /// + public virtual async Task AccessFailed(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + // If this puts the user over the threshold for lockout, lock them out and reset the access failed count + var count = await store.IncrementAccessFailedCount(user).ConfigureAwait(false); + if (count >= MaxFailedAccessAttemptsBeforeLockout) + { + await + store.SetLockoutEndDate(user, DateTimeOffset.UtcNow.Add(DefaultAccountLockoutTimeSpan)) + .ConfigureAwait(false); + await store.ResetAccessFailedCount(user).ConfigureAwait(false); + } + return await Update(user).ConfigureAwait(false); + } + + /// + /// Resets the access failed count for the user to 0 + /// + /// + /// + public virtual async Task ResetAccessFailedCount(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + await store.ResetAccessFailedCount(user).ConfigureAwait(false); + return await Update(user).ConfigureAwait(false); + } + + /// + /// Returns the number of failed access attempts for the user + /// + /// + /// + public virtual async Task GetAccessFailedCount(TKey userId) + { + ThrowIfDisposed(); + var store = GetUserLockoutStore(); + var user = await FindById(userId).ConfigureAwait(false); + if (user == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.UserIdNotFound, + userId)); + } + return await store.GetAccessFailedCount(user).ConfigureAwait(false); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// When disposing, actually dipose the store context + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + Store.Dispose(); + _disposed = true; + } + } + } +} \ No newline at end of file