diff --git a/build.cmd b/build.cmd index 86ca5bbbf1..220a1ff561 100644 --- a/build.cmd +++ b/build.cmd @@ -19,10 +19,10 @@ IF EXIST packages\KoreBuild goto run .nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre .nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion -IF "%SKIP_KRE_INSTALL%"=="1" goto run -CALL packages\KoreBuild\build\kvm upgrade -runtime CLR -x86 -CALL packages\KoreBuild\build\kvm install default -runtime CoreCLR -x86 +IF "%SKIP_DOTNET_INSTALL%"=="1" goto run +CALL packages\KoreBuild\build\dotnetsdk upgrade -runtime CLR -x86 +CALL packages\KoreBuild\build\dotnetsdk install default -runtime CoreCLR -x86 :run -CALL packages\KoreBuild\build\kvm use default -runtime CLR -x86 +CALL packages\KoreBuild\build\dotnetsdk use default -runtime CLR -x86 packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh index c7873ef58e..350d7e389a 100644 --- a/build.sh +++ b/build.sh @@ -28,11 +28,11 @@ if test ! -d packages/KoreBuild; then fi if ! type k > /dev/null 2>&1; then - source packages/KoreBuild/build/kvm.sh + source packages/KoreBuild/build/dotnetsdk.sh fi if ! type k > /dev/null 2>&1; then - kvm upgrade + dotnetsdk upgrade fi mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" diff --git a/samples/IdentitySample.Mvc/Controllers/AccountController.cs b/samples/IdentitySample.Mvc/Controllers/AccountController.cs index 728328eed1..48ad2824f4 100644 --- a/samples/IdentitySample.Mvc/Controllers/AccountController.cs +++ b/samples/IdentitySample.Mvc/Controllers/AccountController.cs @@ -1,11 +1,10 @@ -using Microsoft.AspNet.Http; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.Mvc.Rendering; -using System.Linq; +using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Rendering; namespace IdentitySample.Models { @@ -43,20 +42,23 @@ namespace IdentitySample.Models ViewBag.ReturnUrl = returnUrl; if (ModelState.IsValid) { - var signInStatus = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false); - switch (signInStatus) + var result = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false); + if (result.Succeeded) { - case SignInStatus.Success: - return RedirectToLocal(returnUrl); - case SignInStatus.LockedOut: - ModelState.AddModelError("", "User is locked out, try again later."); - return View(model); - case SignInStatus.RequiresVerification: - return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); - case SignInStatus.Failure: - default: - ModelState.AddModelError("", "Invalid username or password."); - return View(model); + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + ModelState.AddModelError("", "Invalid username or password."); + return View(model); } } @@ -143,22 +145,26 @@ namespace IdentitySample.Models // Sign in the user with this external login provider if the user already has a login var result = await SignInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); - switch (result) + if (result.Succeeded) { - case SignInStatus.Success: - return RedirectToLocal(returnUrl); - case SignInStatus.LockedOut: - return View("Lockout"); - case SignInStatus.RequiresVerification: - return RedirectToAction("SendCode", new { ReturnUrl = returnUrl }); - case SignInStatus.Failure: - default: - // If the user does not have an account, then prompt the user to create an account - ViewBag.ReturnUrl = returnUrl; - ViewBag.LoginProvider = info.LoginProvider; - // REVIEW: handle case where email not in claims? - var email = info.ExternalIdentity.FindFirstValue(ClaimTypes.Email); - return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction("SendCode", new { ReturnUrl = returnUrl }); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + // If the user does not have an account, then prompt the user to create an account + ViewBag.ReturnUrl = returnUrl; + ViewBag.LoginProvider = info.LoginProvider; + // REVIEW: handle case where email not in claims? + var email = info.ExternalIdentity.FindFirstValue(ClaimTypes.Email); + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); } } @@ -380,15 +386,18 @@ namespace IdentitySample.Models } var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); - switch (result) + if (result.Succeeded) { - case SignInStatus.Success: - return RedirectToLocal(model.ReturnUrl); - case SignInStatus.LockedOut: - return View("Lockout"); - default: - ModelState.AddModelError("", "Invalid code."); - return View(model); + return RedirectToLocal(model.ReturnUrl); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + ModelState.AddModelError("", "Invalid code."); + return View(model); } } @@ -398,7 +407,7 @@ namespace IdentitySample.Models { foreach (var error in result.Errors) { - ModelState.AddModelError("", error); + ModelState.AddModelError("", error.Description); } } diff --git a/samples/IdentitySample.Mvc/Controllers/ManageController.cs b/samples/IdentitySample.Mvc/Controllers/ManageController.cs index e67342fb0a..5a4359dd11 100644 --- a/samples/IdentitySample.Mvc/Controllers/ManageController.cs +++ b/samples/IdentitySample.Mvc/Controllers/ManageController.cs @@ -346,7 +346,7 @@ namespace IdentitySample { foreach (var error in result.Errors) { - ModelState.AddModelError("", error); + ModelState.AddModelError("", error.Description); } } diff --git a/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj b/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj index 0c84b42be0..3cdababc47 100644 --- a/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj +++ b/samples/IdentitySample.Mvc/IdentitySample.Mvc.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -12,6 +12,7 @@ 2.0 + 5131 - + \ No newline at end of file diff --git a/samples/IdentitySample.Mvc/LocalConfig.json b/samples/IdentitySample.Mvc/LocalConfig.json index 06b6876a05..49b1ac00a4 100644 --- a/samples/IdentitySample.Mvc/LocalConfig.json +++ b/samples/IdentitySample.Mvc/LocalConfig.json @@ -3,7 +3,7 @@ "DefaultAdminPassword": "YouShouldChangeThisPassword1!", "Data": { "IdentityConnection": { - "Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=IdentityMvc-11-24-14;Trusted_Connection=True;MultipleActiveResultSets=true" + "Connectionstring": "Server=(localdb)\\mssqllocaldb;Database=IdentityMvc-1-7-15;Trusted_Connection=True;MultipleActiveResultSets=true" } }, "Identity": { diff --git a/samples/IdentitySample.Mvc/Views/Home/Index.cshtml b/samples/IdentitySample.Mvc/Views/Home/Index.cshtml index 419bedfccb..65e438ad78 100644 --- a/samples/IdentitySample.Mvc/Views/Home/Index.cshtml +++ b/samples/IdentitySample.Mvc/Views/Home/Index.cshtml @@ -72,7 +72,7 @@ Add Social Logins
  • - Get more data about the user when they login suing Facebook + Get more data about the user when they log in using Facebook
  • @@ -102,7 +102,7 @@
    Account Confirmation
    When you register a new account, you will be sent an email confirmation. - You can use an email service such as SendGrid which integrate nicely with Windows Azure and requires no configuration or + You can use an email service such as SendGrid which integrates nicely with Windows Azure and requires no configuration or set up an SMTP server to send email. You can send email using the EmailService which is registered in App_Start\IdentityConfig.cs
    diff --git a/samples/IdentitySample.Mvc/web.config b/samples/IdentitySample.Mvc/web.config index 7a210ad15b..71ebc78e72 100644 --- a/samples/IdentitySample.Mvc/web.config +++ b/samples/IdentitySample.Mvc/web.config @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs b/src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs index add648bd0b..5ea2553f6d 100644 --- a/src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs +++ b/src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs @@ -30,45 +30,30 @@ namespace Microsoft.AspNet.Identity.EntityFramework { b.Key(u => u.Id); b.ForRelational().Table("AspNetUsers"); + b.Property(u => u.ConcurrencyStamp).ConcurrencyToken(); }); builder.Entity(b => { b.Key(r => r.Id); b.ForRelational().Table("AspNetRoles"); + b.Property(r => r.ConcurrencyStamp).ConcurrencyToken(); }); builder.Entity>(b => { b.Key(uc => uc.Id); - b.ManyToOne().ForeignKey(uc => uc.UserId); + b.HasOne().WithMany().ForeignKey(uc => uc.UserId); b.ForRelational().Table("AspNetUserClaims"); }); builder.Entity>(b => { b.Key(rc => rc.Id); - b.ManyToOne().ForeignKey(rc => rc.RoleId); + b.HasOne().WithMany().ForeignKey(rc => rc.RoleId); b.ForRelational().Table("AspNetRoleClaims"); }); - var userType = builder.Model.GetEntityType(typeof(TUser)); - var roleType = builder.Model.GetEntityType(typeof(TRole)); - var userClaimType = builder.Model.GetEntityType(typeof(IdentityUserClaim)); - var roleClaimType = builder.Model.GetEntityType(typeof(IdentityRoleClaim)); - var userRoleType = builder.Model.GetEntityType(typeof(IdentityUserRole)); - //var ucfk = userClaimType.GetOrAddForeignKey(userType.GetPrimaryKey(), new[] { userClaimType.GetProperty("UserId") }); - //userType.AddNavigation(new Navigation(ucfk, "Claims", false)); - //userClaimType.AddNavigation(new Navigation(ucfk, "User", true)); - //var urfk = userRoleType.GetOrAddForeignKey(userType.GetPrimaryKey(), new[] { userRoleType.GetProperty("UserId") }); - //userType.AddNavigation(new Navigation(urfk, "Roles", false)); - - //var urfk2 = userRoleType.GetOrAddForeignKey(roleType.GetPrimaryKey(), new[] { userRoleType.GetProperty("RoleId") }); - //roleType.AddNavigation(new Navigation(urfk2, "Users", false)); - - var rcfk = roleClaimType.GetOrAddForeignKey(new[] { roleClaimType.GetProperty("RoleId") }, roleType.GetPrimaryKey()); - roleType.AddNavigation("Claims", rcfk, false); - builder.Entity>(b => { b.Key(r => new { r.UserId, r.RoleId }); @@ -81,7 +66,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework builder.Entity>(b => { b.Key(l => new { l.LoginProvider, l.ProviderKey }); - b.ManyToOne().ForeignKey(uc => uc.UserId); + b.HasOne().WithMany().ForeignKey(uc => uc.UserId); b.ForRelational().Table("AspNetUserLogins"); }); } diff --git a/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs b/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs index 53cab96a6a..89cf656d5a 100644 --- a/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs +++ b/src/Microsoft.AspNet.Identity.EntityFramework/RoleStore.cs @@ -4,41 +4,42 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Update; namespace Microsoft.AspNet.Identity.EntityFramework { public class RoleStore : RoleStore where TRole : IdentityRole { - public RoleStore(DbContext context) : base(context) { } + public RoleStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } } public class RoleStore : RoleStore where TRole : IdentityRole where TContext : DbContext { - public RoleStore(TContext context) : base(context) { } + public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } } - public class RoleStore : - IQueryableRoleStore, + public class RoleStore : + IQueryableRoleStore, IRoleClaimStore where TRole : IdentityRole where TKey : IEquatable where TContext : DbContext { - public RoleStore(TContext context) + public RoleStore(TContext context, IdentityErrorDescriber describer = null) { if (context == null) { throw new ArgumentNullException("context"); } Context = context; + ErrorDescriber = describer ?? new IdentityErrorDescriber(); } private bool _disposed; @@ -46,6 +47,11 @@ namespace Microsoft.AspNet.Identity.EntityFramework public TContext Context { get; private set; } + /// + /// Used to generate public API error messages + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + /// /// If true will call SaveChanges after CreateAsync/UpdateAsync/DeleteAsync /// @@ -59,12 +65,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework } } - public virtual Task GetRoleAggregate(Expression> filter, CancellationToken cancellationToken = default(CancellationToken)) - { - return Roles.FirstOrDefaultAsync(filter); - } - - public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -74,9 +75,10 @@ namespace Microsoft.AspNet.Identity.EntityFramework } await Context.AddAsync(role, cancellationToken); await SaveChanges(cancellationToken); + return IdentityResult.Success; } - public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -84,11 +86,21 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("role"); } + Context.Attach(role); + role.ConcurrencyStamp = Guid.NewGuid().ToString(); Context.Update(role); - await SaveChanges(cancellationToken); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; } - public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -97,7 +109,15 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("role"); } Context.Remove(role); - await SaveChanges(cancellationToken); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; } public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) @@ -163,20 +183,43 @@ namespace Microsoft.AspNet.Identity.EntityFramework cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); var roleId = ConvertIdFromString(id); - return GetRoleAggregate(u => u.Id.Equals(roleId), cancellationToken); + return Roles.FirstOrDefaultAsync(u => u.Id.Equals(roleId), cancellationToken); } /// - /// Find a role by name + /// Find a role by normalized name /// - /// + /// /// /// - public virtual Task FindByNameAsync(string name, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - return GetRoleAggregate(u => u.Name.ToUpper() == name.ToUpper(), cancellationToken); + return Roles.FirstOrDefaultAsync(r => r.NormalizedName == normalizedName, cancellationToken); + } + + public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.NormalizedName); + } + + public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + role.NormalizedName = normalizedName; + return Task.FromResult(0); } private void ThrowIfDisposed() @@ -203,7 +246,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("role"); } - return await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(); + return await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken); } public Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) @@ -218,7 +261,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("claim"); } - return RoleClaims.AddAsync(new IdentityRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }); + return RoleClaims.AddAsync(new IdentityRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }, cancellationToken); } public async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) @@ -232,7 +275,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("claim"); } - var claims = await RoleClaims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(); + var claims = await RoleClaims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); foreach (var c in claims) { RoleClaims.Remove(c); diff --git a/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs b/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs index 18e9b9d87f..74e6ddb234 100644 --- a/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs +++ b/src/Microsoft.AspNet.Identity.EntityFramework/UserStore.cs @@ -5,23 +5,23 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Linq.Expressions; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Update; namespace Microsoft.AspNet.Identity.EntityFramework { public class UserStore : UserStore { - public UserStore(DbContext context) : base(context) { } + public UserStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } } public class UserStore : UserStore where TUser : IdentityUser, new() { - public UserStore(DbContext context) : base(context) { } + public UserStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } } public class UserStore : UserStore @@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework where TRole : IdentityRole, new() where TContext : DbContext { - public UserStore(TContext context) : base(context) { } + public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } } public class UserStore : @@ -49,19 +49,25 @@ namespace Microsoft.AspNet.Identity.EntityFramework where TKey : IEquatable { - public UserStore(TContext context) + public UserStore(TContext context, IdentityErrorDescriber describer = null) { if (context == null) { throw new ArgumentNullException("context"); } Context = context; + ErrorDescriber = describer ?? new IdentityErrorDescriber(); } private bool _disposed; public TContext Context { get; private set; } + /// + /// Used to generate public API error messages + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + /// /// If true will call SaveChanges after CreateAsync/UpdateAsync/DeleteAsync /// @@ -72,15 +78,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : Task.FromResult(0); } - protected virtual Task GetUserAggregate(Expression> filter, CancellationToken cancellationToken = default(CancellationToken)) - { - return Users.FirstOrDefaultAsync(filter, cancellationToken); - // TODO: .Include(u => u.Roles) - //.Include(u => u.Claims) - //.Include(u => u.Logins); - } - - public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -91,7 +89,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(ConvertIdToString(user.Id)); } - public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -102,7 +100,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(user.UserName); } - public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -114,7 +112,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(0); } - public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -125,7 +123,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(user.NormalizedUserName); } - public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -137,7 +135,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(0); } - public async virtual Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -147,9 +145,10 @@ namespace Microsoft.AspNet.Identity.EntityFramework } await Context.AddAsync(user, cancellationToken); await SaveChanges(cancellationToken); + return IdentityResult.Success; } - public async virtual Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -157,11 +156,22 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("user"); } + + Context.Attach(user); + user.ConcurrencyStamp = Guid.NewGuid().ToString(); Context.Update(user); - await SaveChanges(cancellationToken); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; } - public async virtual Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -169,8 +179,17 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("user"); } + Context.Remove(user); - await SaveChanges(cancellationToken); + try + { + await SaveChanges(cancellationToken); + } + catch (DbUpdateConcurrencyException) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; } /// @@ -184,7 +203,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); var id = ConvertIdFromString(userId); - return GetUserAggregate(u => u.Id.Equals(id), cancellationToken); + return Users.FirstOrDefaultAsync(u => u.Id.Equals(id), cancellationToken); } public virtual TKey ConvertIdFromString(string id) @@ -215,7 +234,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - return GetUserAggregate(u => u.NormalizedUserName == normalizedUserName, cancellationToken); + return Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken); } public IQueryable Users @@ -290,16 +309,13 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, "roleName"); } - var roleEntity = await Roles.SingleOrDefaultAsync(r => r.Name.ToUpper() == roleName.ToUpper()); + var roleEntity = await Roles.SingleOrDefaultAsync(r => r.Name.ToUpper() == roleName.ToUpper(), cancellationToken); if (roleEntity == null) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, Resources.RoleNotFound, roleName)); } var ur = new IdentityUserRole { UserId = user.Id, RoleId = roleEntity.Id }; - // TODO: rely on fixup? await UserRoles.AddAsync(ur); - user.Roles.Add(ur); - roleEntity.Users.Add(ur); } /// @@ -321,14 +337,13 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, "roleName"); } - var roleEntity = await Roles.SingleOrDefaultAsync(r => r.Name.ToUpper() == roleName.ToUpper()); + var roleEntity = await Roles.SingleOrDefaultAsync(r => r.Name.ToUpper() == roleName.ToUpper(), cancellationToken); if (roleEntity != null) { - var userRole = await UserRoles.FirstOrDefaultAsync(r => roleEntity.Id.Equals(r.RoleId) && r.UserId.Equals(user.Id)); + var userRole = await UserRoles.FirstOrDefaultAsync(r => roleEntity.Id.Equals(r.RoleId) && r.UserId.Equals(user.Id), cancellationToken); if (userRole != null) { UserRoles.Remove(userRole); - user.Roles.Remove(userRole); } } } @@ -339,7 +354,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework /// /// /// - public virtual Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public virtual async Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -348,12 +363,11 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("user"); } var userId = user.Id; -// TODO: var query = from userRole in UserRoles - var query = from userRole in user.Roles + var query = from userRole in UserRoles join role in Roles on userRole.RoleId equals role.Id + where userRole.UserId.Equals(userId) select role.Name; - //return await query.ToListAsync(); - return Task.FromResult>(query.ToList()); + return await query.ToListAsync(); } /// @@ -371,18 +385,16 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("user"); } - if (String.IsNullOrWhiteSpace(roleName)) + if (string.IsNullOrWhiteSpace(roleName)) { throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, "roleName"); } - var role = await Roles.SingleOrDefaultAsync(r => r.Name.ToUpper() == roleName.ToUpper()); + var role = await Roles.SingleOrDefaultAsync(r => r.Name.ToUpper() == roleName.ToUpper(), cancellationToken); if (role != null) { var userId = user.Id; var roleId = role.Id; - return user.Roles.Any(ur => ur.RoleId.Equals(roleId)); - //return await UserRoles.AnyAsync(ur => ur.RoleId.Equals(roleId) && ur.UserId.Equals(userId)); - //return UserRoles.Any(ur => ur.RoleId.Equals(roleId) && ur.UserId.Equals(userId)); + return await UserRoles.AnyAsync(ur => ur.RoleId.Equals(roleId) && ur.UserId.Equals(userId)); } return false; } @@ -408,7 +420,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework private DbSet> UserRoles { get { return Context.Set>(); } } private DbSet> UserLogins { get { return Context.Set>(); } } - public async Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -416,10 +428,10 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("user"); } - return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(); + return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken); } - public async Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -432,11 +444,11 @@ namespace Microsoft.AspNet.Identity.EntityFramework } foreach (var claim in claims) { - await UserClaims.AddAsync(new IdentityUserClaim { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value }); + await UserClaims.AddAsync(new IdentityUserClaim { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value }, cancellationToken); } } - public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -452,15 +464,15 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("newClaim"); } - var matchedClaims = await UserClaims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(); - foreach(var matchedClaim in matchedClaims) + var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); + foreach (var matchedClaim in matchedClaims) { matchedClaim.ClaimValue = newClaim.Value; matchedClaim.ClaimType = newClaim.Type; } } - public async Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + public async virtual Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); if (user == null) @@ -471,8 +483,9 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("claims"); } - foreach (var claim in claims) { - var matchedClaims = await UserClaims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(); + foreach (var claim in claims) + { + var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken); foreach (var c in matchedClaims) { UserClaims.Remove(c); @@ -502,7 +515,6 @@ namespace Microsoft.AspNet.Identity.EntityFramework }; // TODO: fixup so we don't have to update both await UserLogins.AddAsync(l); - user.Logins.Add(l); } public virtual async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, @@ -515,12 +527,10 @@ namespace Microsoft.AspNet.Identity.EntityFramework throw new ArgumentNullException("user"); } var userId = user.Id; - // todo: ensure logins loaded - var entry = await UserLogins.SingleOrDefaultAsync(l => l.UserId.Equals(userId) && l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + var entry = await UserLogins.SingleOrDefaultAsync(l => l.UserId.Equals(userId) && l.LoginProvider == loginProvider && l.ProviderKey == providerKey, cancellationToken); if (entry != null) { UserLogins.Remove(entry); - user.Logins.Remove(entry); } } @@ -532,13 +542,9 @@ namespace Microsoft.AspNet.Identity.EntityFramework { throw new ArgumentNullException("user"); } - // todo: ensure logins loaded - //IList result = user.Logins - // .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToList(); var userId = user.Id; - return await UserLogins.Where(l => l.UserId.Equals(userId)) - .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(); + .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(cancellationToken); } public async virtual Task FindByLoginAsync(string loginProvider, string providerKey, @@ -546,12 +552,11 @@ namespace Microsoft.AspNet.Identity.EntityFramework { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - // todo: ensure logins loaded var userLogin = await - UserLogins.FirstOrDefaultAsync(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + UserLogins.FirstOrDefaultAsync(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey, cancellationToken); if (userLogin != null) { - return await GetUserAggregate(u => u.Id.Equals(userLogin.UserId), cancellationToken); + return await Users.FirstOrDefaultAsync(u => u.Id.Equals(userLogin.UserId), cancellationToken); } return null; } @@ -628,19 +633,40 @@ namespace Microsoft.AspNet.Identity.EntityFramework return Task.FromResult(user.Email); } + public virtual Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + return Task.FromResult(user.NormalizedEmail); + } + + public virtual Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException("user"); + } + user.NormalizedEmail = normalizedEmail; + return Task.FromResult(0); + } + /// /// Find an user by email /// /// /// /// - public virtual Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) + public virtual Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - return GetUserAggregate(u => u.Email == email, cancellationToken); - // todo: ToUpper blows up with Null Ref - //return GetUserAggregate(u => u.Email.ToUpper() == email.ToUpper(), cancellationToken); + return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken); } /// @@ -913,5 +939,58 @@ namespace Microsoft.AspNet.Identity.EntityFramework } return Task.FromResult(user.TwoFactorEnabled); } + + /// + /// Get all users with given claim + /// + /// + /// + /// + public async virtual Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + + var query = from userclaims in UserClaims + join user in Users on userclaims.UserId equals user.Id + where userclaims.ClaimValue == claim.Value + && userclaims.ClaimType == claim.Type + select user; + + return await query.ToListAsync(cancellationToken); + } + + /// + /// Get all users in given role + /// + /// + /// + /// + public async virtual Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (String.IsNullOrEmpty(roleName)) + { + throw new ArgumentNullException("role"); + } + + var role = await Roles.Where(x => x.Name.Equals(roleName)).FirstOrDefaultAsync(cancellationToken); + + if (role != null) + { + var query = from userrole in UserRoles + join user in Users on userrole.UserId equals user.Id + where userrole.RoleId.Equals(role.Id) + select user; + + return await query.ToListAsync(cancellationToken); + } + return new List(); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/BuilderExtensions.cs b/src/Microsoft.AspNet.Identity/BuilderExtensions.cs index 4049f04ffd..d4d184e759 100644 --- a/src/Microsoft.AspNet.Identity/BuilderExtensions.cs +++ b/src/Microsoft.AspNet.Identity/BuilderExtensions.cs @@ -2,15 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; + using Microsoft.AspNet.Identity; namespace Microsoft.AspNet.Builder { /// - /// Startup extensions + /// Identity extensions for . /// public static class BuilderExtensions { + /// + /// Enables the ASP.NET identity for the current application. + /// + /// The instance this method extends. + /// The instance this method extends. public static IApplicationBuilder UseIdentity(this IApplicationBuilder app) { if (app == null) diff --git a/src/Microsoft.AspNet.Identity/ClaimsIdentityExtensions.cs b/src/Microsoft.AspNet.Identity/ClaimsIdentityExtensions.cs index 63cd7b03f3..23655ddcff 100644 --- a/src/Microsoft.AspNet.Identity/ClaimsIdentityExtensions.cs +++ b/src/Microsoft.AspNet.Identity/ClaimsIdentityExtensions.cs @@ -6,15 +6,16 @@ using System.Security.Claims; namespace System.Security.Principal { /// - /// Extensions making it easier to get the user name/user id claims off of an identity + /// Claims related extensions for . /// public static class ClaimsIdentityExtensions { /// - /// Return the user name using the UserNameClaimType + /// Returns the Name claim value if present otherwise returns null. /// - /// - /// + /// The instance this method extends. + /// The Name claim value, or null if the claim is not present. + /// The name claim is identified by . public static string GetUserName(this IIdentity identity) { if (identity == null) @@ -26,10 +27,11 @@ namespace System.Security.Principal } /// - /// Return the user id using the UserIdClaimType + /// Returns the User ID claim value if present otherwise returns null. /// - /// - /// + /// The instance this method extends. + /// The User ID claim value, or null if the claim is not present. + /// The name claim is identified by . public static string GetUserId(this IIdentity identity) { if (identity == null) @@ -41,11 +43,11 @@ namespace System.Security.Principal } /// - /// Return the claim value for the first claim with the specified type if it exists, null otherwise + /// Returns the value for the first claim of the specified type otherwise null the claim is not present. /// - /// - /// - /// + /// The instance this method extends. + /// The claim type whose first value should be returned. + /// The value of the first instance of the specifed claim type, or null if the claim is not present. public static string FindFirstValue(this ClaimsIdentity identity, string claimType) { if (identity == null) diff --git a/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs b/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs index 9fc5cb760f..bda8839ac8 100644 --- a/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs +++ b/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs @@ -5,19 +5,29 @@ using System; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; + using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Identity { /// - /// Creates a ClaimsIdentity from a User + /// Provides methods to create a claims identity for a given user. /// - /// + /// The type used to represent a user. + /// The type used to represent a role. public class ClaimsIdentityFactory : IClaimsIdentityFactory where TUser : class where TRole : class { - public ClaimsIdentityFactory(UserManager userManager, RoleManager roleManager, + /// + /// Initializes a new instance of the class. + /// + /// The to retrieve user information from. + /// The to retrieve a user's roles from. + /// The configured . + public ClaimsIdentityFactory( + UserManager userManager, + RoleManager roleManager, IOptions optionsAccessor) { if (userManager == null) @@ -37,19 +47,38 @@ namespace Microsoft.AspNet.Identity Options = optionsAccessor.Options; } + /// + /// Gets the for this factory. + /// + /// + /// The current for this factory instance. + /// public UserManager UserManager { get; private set; } + + /// + /// Gets the for this factory. + /// + /// + /// The current for this factory instance. + /// public RoleManager RoleManager { get; private set; } + + /// + /// Gets the for this factory. + /// + /// + /// The current for this factory instance. + /// public IdentityOptions Options { get; private set; } /// - /// CreateAsync a ClaimsIdentity from a user + /// Creates a populated for the specified . /// - /// - /// - /// - /// - /// - public virtual async Task CreateAsync(TUser user, + /// The user instance to create claims on. + /// A to observe while waiting for the tasks to complete. + /// A that represents the started task. + public virtual async Task CreateAsync( + TUser user, CancellationToken cancellationToken = default(CancellationToken)) { if (user == null) diff --git a/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs b/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs index 9b8dd3c24f..45e4739bcc 100644 --- a/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs +++ b/src/Microsoft.AspNet.Identity/ClaimsIdentityOptions.cs @@ -5,28 +5,41 @@ using System.Security.Claims; namespace Microsoft.AspNet.Identity { + /// + /// Options for ClaimType names. + /// public class ClaimsIdentityOptions { - public static readonly string DefaultSecurityStampClaimType = "AspNet.Identity.SecurityStamp"; - /// - /// Claim type used for role claims + /// Gets the ClaimType used for a Role claim. /// + /// + /// This defaults to . + /// public string RoleClaimType { get; set; } = ClaimTypes.Role; /// - /// Claim type used for the user name + /// Gets the ClaimType used for the user name claim. /// + /// + /// This defaults to . + /// public string UserNameClaimType { get; set; } = ClaimTypes.Name; /// - /// Claim type used for the user id + /// Gets the ClaimType used for the user identifier claim. /// + /// + /// This defaults to . + /// public string UserIdClaimType { get; set; } = ClaimTypes.NameIdentifier; /// - /// Claim type used for the user security stamp + /// Gets the ClaimType used for the security stamp claim.. /// - public string SecurityStampClaimType { get; set; } = DefaultSecurityStampClaimType; + /// + /// This defaults to "AspNet.Identity.SecurityStamp". + /// + public string SecurityStampClaimType { get; set; } = "AspNet.Identity.SecurityStamp"; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs b/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs index ae7bdfc960..988b569e32 100644 --- a/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs +++ b/src/Microsoft.AspNet.Identity/DataProtectionTokenProvider.cs @@ -1,51 +1,71 @@ -using Microsoft.AspNet.Security.DataProtection; -using Microsoft.Framework.OptionsModel; +// Copyright (c) Microsoft Open Technologies, Inc. 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.IO; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.Framework.OptionsModel; + namespace Microsoft.AspNet.Identity { - public class DataProtectionTokenProviderOptions - { - public string Name { get; set; } = "DataProtection"; - public TimeSpan TokenLifespan { get; set; } = TimeSpan.FromDays(1); - } - /// - /// Token provider that uses an IDataProtector to generate encrypted tokens based off of the security stamp + /// Provides protection and validation of identity tokens. /// + /// The type used to represent a user. public class DataProtectorTokenProvider : IUserTokenProvider where TUser : class { + /// + /// Initializes a new instance of the class. + /// + /// The system data protection provider. + /// The configured . public DataProtectorTokenProvider(IDataProtectionProvider dataProtectionProvider, IOptions options) { - if (options == null || options.Options == null) - { - throw new ArgumentNullException(nameof(options)); - } if (dataProtectionProvider == null) { throw new ArgumentNullException(nameof(dataProtectionProvider)); } - Options = options.Options; + Options = options?.Options ?? new DataProtectionTokenProviderOptions(); // Use the Name as the purpose which should usually be distinct from others Protector = dataProtectionProvider.CreateProtector(Name ?? "DataProtectorTokenProvider"); } - public DataProtectionTokenProviderOptions Options { get; private set; } - public IDataProtector Protector { get; private set; } + /// + /// Gets the for this instance. + /// + /// + /// The for this instance. + /// + protected DataProtectionTokenProviderOptions Options { get; private set; } + /// + /// Gets the for this instance. + /// + /// + /// The for this instance. + /// + protected IDataProtector Protector { get; private set; } + + /// + /// Gets the name of this instance. + /// + /// + /// The name of this instance. + /// public string Name { get { return Options.Name; } } /// - /// Generate a protected string for a user + /// Generates a protected token for the specified . /// - /// - /// - /// - /// + /// The purpose the token will be used for. + /// The to retrieve user properties from. + /// The the token will be generated from. + /// A to observe while waiting for the tasks to complete. + /// A that contains the protected token. public async Task GenerateAsync(string purpose, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { @@ -72,13 +92,14 @@ namespace Microsoft.AspNet.Identity } /// - /// Return false if the token is not valid + /// Validates the protected for the specified and . /// - /// - /// - /// - /// - /// + /// The purpose the token was be used for. + /// The token to validate. + /// The to retrieve user properties from. + /// The the token was generated for. + /// A to observe while waiting for the tasks to complete. + /// A that is true if the token is valid, otherwise false. public async Task ValidateAsync(string purpose, string token, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { @@ -128,11 +149,13 @@ namespace Microsoft.AspNet.Identity } /// - /// Returns false because tokens are two long to be used for two factor + /// Returns a indicating whether a token generated by this instance + /// can be used as a Two Factor Authentication token. /// - /// - /// - /// + /// The to retrieve user properties from. + /// The the token was generated for. + /// True if a token generated by this instance can be used as a Two Factor Authentication token, otherwise false. + /// This method will always return false for instances of . public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { @@ -140,12 +163,13 @@ namespace Microsoft.AspNet.Identity } /// - /// This provider no-ops by default when asked to notify a user + /// Creates a notification task for A based on the supplied . /// - /// - /// - /// - /// + /// The token to generate notifications for.. + /// The to retrieve user properties from. + /// The the token was generated for. + /// A to observe while waiting for the tasks to complete. + /// A that represents the started task. public Task NotifyAsync(string token, UserManager manager, TUser user, CancellationToken cancellationToken = default(CancellationToken)) { @@ -153,7 +177,9 @@ namespace Microsoft.AspNet.Identity } } - // Based on Levi's authentication sample + /// + /// Utility extensions to streams + /// internal static class StreamExtensions { internal static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); diff --git a/src/Microsoft.AspNet.Identity/DataProtectionTokenProviderOptions.cs b/src/Microsoft.AspNet.Identity/DataProtectionTokenProviderOptions.cs new file mode 100644 index 0000000000..57d589440a --- /dev/null +++ b/src/Microsoft.AspNet.Identity/DataProtectionTokenProviderOptions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Contains options for the . + /// + public class DataProtectionTokenProviderOptions + { + /// + /// Gets or sets the name of the . + /// + /// + /// The name of the . + /// + public string Name { get; set; } = "DataProtectorTokenProvider"; + + /// + /// Gets or sets the amount of time a generated token remains valid. + /// + /// + /// The amount of time a generated token remains valid. + /// + public TimeSpan TokenLifespan { get; set; } = TimeSpan.FromDays(1); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserNameNormalizer.cs b/src/Microsoft.AspNet.Identity/ILookupNormalizer.cs similarity index 61% rename from src/Microsoft.AspNet.Identity/IUserNameNormalizer.cs rename to src/Microsoft.AspNet.Identity/ILookupNormalizer.cs index b8d1bfcbf2..159f222ffa 100644 --- a/src/Microsoft.AspNet.Identity/IUserNameNormalizer.cs +++ b/src/Microsoft.AspNet.Identity/ILookupNormalizer.cs @@ -4,15 +4,15 @@ namespace Microsoft.AspNet.Identity { /// - /// Used to normalize a user name + /// Used to normalize keys for consistent lookups /// - public interface IUserNameNormalizer + public interface ILookupNormalizer { /// - /// Returns the normalized user name + /// Returns the normalized key /// - /// + /// /// - string Normalize(string userName); + string Normalize(string key); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IRoleStore.cs b/src/Microsoft.AspNet.Identity/IRoleStore.cs index 4c4fc16f46..405a47ce31 100644 --- a/src/Microsoft.AspNet.Identity/IRoleStore.cs +++ b/src/Microsoft.AspNet.Identity/IRoleStore.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); /// /// Update a role @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); /// /// DeleteAsync a role @@ -35,7 +35,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); + Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)); /// /// Returns a role's id @@ -63,6 +63,26 @@ namespace Microsoft.AspNet.Identity Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Get a role's normalized name + /// + /// + /// + /// + Task GetNormalizedRoleNameAsync(TRole role, + CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Set a role's normalized name + /// + /// + /// + /// + /// + Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, + CancellationToken cancellationToken = default(CancellationToken)); + + /// /// Finds a role by id /// @@ -72,11 +92,11 @@ namespace Microsoft.AspNet.Identity Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Find a role by name + /// Find a role by normalized name /// - /// + /// /// /// - Task FindByNameAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)); + Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserClaimStore.cs b/src/Microsoft.AspNet.Identity/IUserClaimStore.cs index eb7d0d559f..825c289b0e 100644 --- a/src/Microsoft.AspNet.Identity/IUserClaimStore.cs +++ b/src/Microsoft.AspNet.Identity/IUserClaimStore.cs @@ -51,5 +51,13 @@ namespace Microsoft.AspNet.Identity /// Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Get users having a specific claim + /// + /// Claim to look up + /// + /// + Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserEmailStore.cs b/src/Microsoft.AspNet.Identity/IUserEmailStore.cs index 6569548ffe..816f9383eb 100644 --- a/src/Microsoft.AspNet.Identity/IUserEmailStore.cs +++ b/src/Microsoft.AspNet.Identity/IUserEmailStore.cs @@ -51,9 +51,28 @@ namespace Microsoft.AspNet.Identity /// /// Returns the user associated with this email /// - /// + /// /// /// - Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)); + Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Returns the normalized email + /// + /// + /// + /// + Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Set the normalized email + /// + /// + /// + /// + /// + Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, + CancellationToken cancellationToken = default(CancellationToken)); + } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserRoleStore.cs b/src/Microsoft.AspNet.Identity/IUserRoleStore.cs index e74ca87fe6..690eeeb45a 100644 --- a/src/Microsoft.AspNet.Identity/IUserRoleStore.cs +++ b/src/Microsoft.AspNet.Identity/IUserRoleStore.cs @@ -51,5 +51,13 @@ namespace Microsoft.AspNet.Identity /// Task IsInRoleAsync(TUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Returns all users in given role + /// + /// + /// + /// + Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IUserStore.cs b/src/Microsoft.AspNet.Identity/IUserStore.cs index 93a8fcae36..d6cd313733 100644 --- a/src/Microsoft.AspNet.Identity/IUserStore.cs +++ b/src/Microsoft.AspNet.Identity/IUserStore.cs @@ -63,7 +63,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); /// /// UpdateAsync a user @@ -71,7 +71,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); /// /// DeleteAsync a user @@ -79,7 +79,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); + Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)); /// /// Finds a user diff --git a/src/Microsoft.AspNet.Identity/IdentityBuilder.cs b/src/Microsoft.AspNet.Identity/IdentityBuilder.cs index 8c9d51332a..4f78d0731d 100644 --- a/src/Microsoft.AspNet.Identity/IdentityBuilder.cs +++ b/src/Microsoft.AspNet.Identity/IdentityBuilder.cs @@ -35,6 +35,12 @@ namespace Microsoft.AspNet.Identity return AddScoped(typeof(IRoleValidator<>).MakeGenericType(RoleType), typeof(T)); } + public IdentityBuilder AddErrorDescriber() where TDescriber : IdentityErrorDescriber + { + Services.AddScoped(); + return this; + } + public IdentityBuilder AddPasswordValidator() where T : class { return AddScoped(typeof(IPasswordValidator<>).MakeGenericType(UserType), typeof(T)); @@ -76,5 +82,15 @@ namespace Microsoft.AspNet.Identity .AddTokenProvider(typeof(PhoneNumberTokenProvider<>).MakeGenericType(UserType)) .AddTokenProvider(typeof(EmailTokenProvider<>).MakeGenericType(UserType)); } + + public IdentityBuilder AddUserManager() where TUserManager : class + { + return AddScoped(typeof(UserManager<>).MakeGenericType(UserType), typeof(TUserManager)); + } + + public IdentityBuilder AddRoleManager() where TRoleManager : class + { + return AddScoped(typeof(RoleManager<>).MakeGenericType(RoleType), typeof(TRoleManager)); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/SignInStatus.cs b/src/Microsoft.AspNet.Identity/IdentityError.cs similarity index 64% rename from src/Microsoft.AspNet.Identity/SignInStatus.cs rename to src/Microsoft.AspNet.Identity/IdentityError.cs index 3247a7dd39..0db1958a8f 100644 --- a/src/Microsoft.AspNet.Identity/SignInStatus.cs +++ b/src/Microsoft.AspNet.Identity/IdentityError.cs @@ -3,12 +3,9 @@ namespace Microsoft.AspNet.Identity { - public enum SignInStatus + public class IdentityError { - Success, - LockedOut, - RequiresVerification, - NotAllowed, - Failure + public string Code { get; set; } + public string Description { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs b/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs new file mode 100644 index 0000000000..39d1677713 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/IdentityErrorDescriber.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Identity +{ + public class IdentityErrorDescriber + { + public static IdentityErrorDescriber Default = new IdentityErrorDescriber(); + + public virtual IdentityError DefaultError() + { + return new IdentityError + { + Code = nameof(DefaultError), + Description = Resources.DefaultError + }; + } + public virtual IdentityError ConcurrencyFailure() + { + return new IdentityError + { + Code = nameof(ConcurrencyFailure), + Description = Resources.ConcurrencyFailure + }; + } + + public virtual IdentityError PasswordMismatch() + { + return new IdentityError + { + Code = nameof(PasswordMismatch), + Description = Resources.PasswordMismatch + }; + } + + public virtual IdentityError InvalidToken() + { + return new IdentityError + { + Code = nameof(InvalidToken), + Description = Resources.InvalidToken + }; + } + + public virtual IdentityError LoginAlreadyAssociated() + { + return new IdentityError + { + Code = nameof(LoginAlreadyAssociated), + Description = Resources.LoginAlreadyAssociated + }; + } + + public virtual IdentityError InvalidUserName(string name) + { + return new IdentityError + { + Code = nameof(InvalidUserName), + Description = Resources.FormatInvalidUserName(name) + }; + } + + public virtual IdentityError InvalidEmail(string email) + { + return new IdentityError + { + Code = nameof(InvalidEmail), + Description = Resources.FormatInvalidEmail(email) + }; + } + + public virtual IdentityError DuplicateUserName(string name) + { + return new IdentityError + { + Code = nameof(DuplicateUserName), + Description = Resources.FormatDuplicateUserName(name) + }; + } + + public virtual IdentityError DuplicateEmail(string email) + { + return new IdentityError + { + Code = nameof(DuplicateEmail), + Description = Resources.FormatDuplicateEmail(email) + }; + } + + public virtual IdentityError InvalidRoleName(string name) + { + return new IdentityError + { + Code = nameof(InvalidRoleName), + Description = Resources.FormatInvalidRoleName(name) + }; + } + + public virtual IdentityError DuplicateRoleName(string name) + { + return new IdentityError + { + Code = nameof(DuplicateRoleName), + Description = Resources.FormatDuplicateRoleName(name) + }; + } + + public virtual IdentityError UserAlreadyHasPassword() + { + return new IdentityError + { + Code = nameof(UserAlreadyHasPassword), + Description = Resources.UserAlreadyHasPassword + }; + } + + public virtual IdentityError UserLockoutNotEnabled() + { + return new IdentityError + { + Code = nameof(UserLockoutNotEnabled), + Description = Resources.UserLockoutNotEnabled + }; + } + + public virtual IdentityError UserAlreadyInRole(string role) + { + return new IdentityError + { + Code = nameof(UserAlreadyInRole), + Description = Resources.FormatUserAlreadyInRole(role) + }; + } + + public virtual IdentityError UserNotInRole(string role) + { + return new IdentityError + { + Code = nameof(UserNotInRole), + Description = Resources.FormatUserNotInRole(role) + }; + } + + public virtual IdentityError PasswordTooShort(int length) + { + return new IdentityError + { + Code = nameof(PasswordTooShort), + Description = Resources.FormatPasswordTooShort(length) + }; + } + + public virtual IdentityError PasswordRequiresNonLetterAndDigit() + { + return new IdentityError + { + Code = nameof(PasswordRequiresNonLetterAndDigit), + Description = Resources.PasswordRequiresNonLetterAndDigit + }; + } + + public virtual IdentityError PasswordRequiresDigit() + { + return new IdentityError + { + Code = nameof(PasswordRequiresDigit), + Description = Resources.PasswordRequiresDigit + }; + } + + public virtual IdentityError PasswordRequiresLower() + { + return new IdentityError + { + Code = nameof(PasswordRequiresLower), + Description = Resources.PasswordRequiresLower + }; + } + + public virtual IdentityError PasswordRequiresUpper() + { + return new IdentityError + { + Code = nameof(PasswordRequiresUpper), + Description = Resources.PasswordRequiresUpper + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityResult.cs b/src/Microsoft.AspNet.Identity/IdentityResult.cs index dbe60c4756..043c605fef 100644 --- a/src/Microsoft.AspNet.Identity/IdentityResult.cs +++ b/src/Microsoft.AspNet.Identity/IdentityResult.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Identity { @@ -11,45 +12,19 @@ namespace Microsoft.AspNet.Identity /// public class IdentityResult { - private static readonly IdentityResult _success = new IdentityResult(true); + private static readonly IdentityResult _success = new IdentityResult { Succeeded = 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.Any()) - { - errors = new[] { Resources.DefaultError }; - } - Succeeded = false; - Errors = errors; - } - - protected IdentityResult(bool success) - { - Succeeded = success; - Errors = new string[0]; - } + private List _errors = new List(); /// /// True if the operation was successful /// - public bool Succeeded { get; private set; } + public bool Succeeded { get; protected set; } /// /// List of errors /// - public IEnumerable Errors { get; private set; } + public IEnumerable Errors { get { return _errors; } } /// /// Static success result @@ -65,9 +40,32 @@ namespace Microsoft.AspNet.Identity /// /// /// - public static IdentityResult Failed(params string[] errors) + public static IdentityResult Failed(params IdentityError[] errors) { - return new IdentityResult(errors); + var result = new IdentityResult { Succeeded = false }; + if (errors != null) + { + result._errors.AddRange(errors); + } + return result; + } + + /// + /// Log Identity result + /// + /// + /// + public virtual void Log(ILogger logger, string message) + { + // TODO: Take logging level as a parameter + if (Succeeded) + { + logger.WriteInformation(Resources.FormatLogIdentityResultSuccess(message)); + } + else + { + logger.WriteWarning(Resources.FormatLogIdentityResultFailure(message, string.Join(",", Errors.Select(x => x.Code).ToList()))); + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityRole.cs b/src/Microsoft.AspNet.Identity/IdentityRole.cs index e9206c92ad..0127acb84d 100644 --- a/src/Microsoft.AspNet.Identity/IdentityRole.cs +++ b/src/Microsoft.AspNet.Identity/IdentityRole.cs @@ -65,5 +65,11 @@ namespace Microsoft.AspNet.Identity /// Role name /// public virtual string Name { get; set; } + public virtual string NormalizedName { get; set; } + + /// + /// A random value that should change whenever a role is persisted to the store + /// + public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs index 86fd009b9e..6ccc4ce6c5 100644 --- a/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Identity/IdentityServiceCollectionExtensions.cs @@ -57,8 +57,10 @@ namespace Microsoft.Framework.DependencyInjection services.TryAdd(describe.Transient, UserValidator>()); services.TryAdd(describe.Transient, PasswordValidator>()); services.TryAdd(describe.Transient, PasswordHasher>()); - services.TryAdd(describe.Transient()); + services.TryAdd(describe.Transient()); services.TryAdd(describe.Transient, RoleValidator>()); + // No interface for the error describer so we can add errors without rev'ing the interface + services.TryAdd(describe.Transient()); services.TryAdd(describe.Scoped>()); services.TryAdd(describe.Scoped, ClaimsIdentityFactory>()); services.TryAdd(describe.Scoped, UserManager>()); diff --git a/src/Microsoft.AspNet.Identity/IdentityUser.cs b/src/Microsoft.AspNet.Identity/IdentityUser.cs index 210791c26f..4392bf7db4 100644 --- a/src/Microsoft.AspNet.Identity/IdentityUser.cs +++ b/src/Microsoft.AspNet.Identity/IdentityUser.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; namespace Microsoft.AspNet.Identity { @@ -37,6 +36,8 @@ namespace Microsoft.AspNet.Identity /// public virtual string Email { get; set; } + public virtual string NormalizedEmail { get; set; } + /// /// True if the email is confirmed, default is false /// @@ -52,6 +53,11 @@ namespace Microsoft.AspNet.Identity /// public virtual string SecurityStamp { get; set; } + /// + /// A random value that should change whenever a user is persisted to the store + /// + public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString(); + /// /// PhoneNumber for the user /// @@ -81,20 +87,5 @@ namespace Microsoft.AspNet.Identity /// Used to record failures for the purposes of lockout /// public virtual int AccessFailedCount { get; set; } - - /// - /// Roles for the user - /// - public virtual ICollection> Roles { get; } = new List>(); - - /// - /// Claims for the user - /// - public virtual ICollection> Claims { get; } = new List>(); - - /// - /// Associated logins for the user - /// - public virtual ICollection> Logins { get; } = new List>(); } } diff --git a/src/Microsoft.AspNet.Identity/Microsoft.AspNet.Identity.kproj b/src/Microsoft.AspNet.Identity/Microsoft.AspNet.Identity.kproj index 318f83366a..10681e7ef3 100644 --- a/src/Microsoft.AspNet.Identity/Microsoft.AspNet.Identity.kproj +++ b/src/Microsoft.AspNet.Identity/Microsoft.AspNet.Identity.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -13,5 +13,13 @@ 2.0 + + False + - + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/PasswordHasher.cs b/src/Microsoft.AspNet.Identity/PasswordHasher.cs index 09557ae983..611ce4a04f 100644 --- a/src/Microsoft.AspNet.Identity/PasswordHasher.cs +++ b/src/Microsoft.AspNet.Identity/PasswordHasher.cs @@ -37,14 +37,11 @@ namespace Microsoft.AspNet.Identity /// Constructs a PasswordHasher using the specified options /// /// - public PasswordHasher(IOptions options) + public PasswordHasher(IOptions optionsAccessor = null) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + var options = optionsAccessor?.Options ?? new PasswordHasherOptions(); - _compatibilityMode = options.Options.CompatibilityMode; + _compatibilityMode = options.CompatibilityMode; switch (_compatibilityMode) { case PasswordHasherCompatibilityMode.IdentityV2: @@ -52,7 +49,7 @@ namespace Microsoft.AspNet.Identity break; case PasswordHasherCompatibilityMode.IdentityV3: - _iterCount = options.Options.IterationCount; + _iterCount = options.IterationCount; if (_iterCount < 1) { throw new InvalidOperationException(Resources.InvalidPasswordHasherIterationCount); @@ -63,7 +60,7 @@ namespace Microsoft.AspNet.Identity throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode); } - _rng = options.Options.Rng; + _rng = options.Rng; } // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. diff --git a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs index e441a7bee0..a52e476f15 100644 --- a/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs +++ b/src/Microsoft.AspNet.Identity/PasswordHasherOptions.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Security.Cryptography; -using Microsoft.AspNet.Security.DataProtection; namespace Microsoft.AspNet.Identity { diff --git a/src/Microsoft.AspNet.Identity/PasswordValidator.cs b/src/Microsoft.AspNet.Identity/PasswordValidator.cs index 2a8bcf730f..1726e26694 100644 --- a/src/Microsoft.AspNet.Identity/PasswordValidator.cs +++ b/src/Microsoft.AspNet.Identity/PasswordValidator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,6 +14,13 @@ namespace Microsoft.AspNet.Identity /// public class PasswordValidator : IPasswordValidator where TUser : class { + public PasswordValidator(IdentityErrorDescriber errors = null) + { + Describer = errors ?? new IdentityErrorDescriber(); + } + + public IdentityErrorDescriber Describer { get; private set; } + /// /// Ensures that the password is of the required length and meets the configured requirements /// @@ -33,33 +39,32 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("manager"); } - var errors = new List(); + var errors = new List(); var options = manager.Options.Password; if (string.IsNullOrWhiteSpace(password) || password.Length < options.RequiredLength) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PasswordTooShort, - options.RequiredLength)); + errors.Add(Describer.PasswordTooShort(options.RequiredLength)); } if (options.RequireNonLetterOrDigit && password.All(IsLetterOrDigit)) { - errors.Add(Resources.PasswordRequireNonLetterOrDigit); + errors.Add(Describer.PasswordRequiresNonLetterAndDigit()); } if (options.RequireDigit && !password.Any(IsDigit)) { - errors.Add(Resources.PasswordRequireDigit); + errors.Add(Describer.PasswordRequiresDigit()); } if (options.RequireLowercase && !password.Any(IsLower)) { - errors.Add(Resources.PasswordRequireLower); + errors.Add(Describer.PasswordRequiresLower()); } if (options.RequireUppercase && !password.Any(IsUpper)) { - errors.Add(Resources.PasswordRequireUpper); + errors.Add(Describer.PasswordRequiresUpper()); } return Task.FromResult(errors.Count == 0 ? IdentityResult.Success - : IdentityResult.Failed(String.Join(" ", errors))); + : IdentityResult.Failed(errors.ToArray())); } /// diff --git a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs index 9c91fd4bd5..2aa5369096 100644 --- a/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Identity/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ namespace Microsoft.AspNet.Identity private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.Identity.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// Optimistic concurrency failure, object has been modified. + /// + internal static string ConcurrencyFailure + { + get { return GetString("ConcurrencyFailure"); } + } + + /// + /// Optimistic concurrency failure, object has been modified. + /// + internal static string FormatConcurrencyFailure() + { + return GetString("ConcurrencyFailure"); + } + /// /// Your security code is: {0} /// @@ -43,7 +59,7 @@ namespace Microsoft.AspNet.Identity } /// - /// Security Code + /// Security code /// internal static string DefaultEmailTokenProviderSubject { @@ -51,7 +67,7 @@ namespace Microsoft.AspNet.Identity } /// - /// Security Code + /// Security code /// internal static string FormatDefaultEmailTokenProviderSubject() { @@ -107,7 +123,7 @@ namespace Microsoft.AspNet.Identity } /// - /// DefaultTokenProvider + /// Default Token Provider /// internal static string DefaultTokenProvider { @@ -115,7 +131,7 @@ namespace Microsoft.AspNet.Identity } /// - /// DefaultTokenProvider + /// Default Token Provider /// internal static string FormatDefaultTokenProvider() { @@ -139,35 +155,35 @@ namespace Microsoft.AspNet.Identity } /// - /// Name {0} is already taken. + /// Role name '{0}' is already taken. /// - internal static string DuplicateName + internal static string DuplicateRoleName { - get { return GetString("DuplicateName"); } + get { return GetString("DuplicateRoleName"); } } /// - /// Name {0} is already taken. + /// Role name '{0}' is already taken. /// - internal static string FormatDuplicateName(object p0) + internal static string FormatDuplicateRoleName(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("DuplicateName"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("DuplicateRoleName"), p0); } /// - /// A user with that external login already exists. + /// User name '{0}' is already taken. /// - internal static string ExternalLoginExists + internal static string DuplicateUserName { - get { return GetString("ExternalLoginExists"); } + get { return GetString("DuplicateUserName"); } } /// - /// A user with that external login already exists. + /// User name '{0}' is already taken. /// - internal static string FormatExternalLoginExists() + internal static string FormatDuplicateUserName(object p0) { - return GetString("ExternalLoginExists"); + return string.Format(CultureInfo.CurrentCulture, GetString("DuplicateUserName"), p0); } /// @@ -218,6 +234,22 @@ namespace Microsoft.AspNet.Identity return GetString("InvalidPasswordHasherIterationCount"); } + /// + /// Role name '{0}' is invalid. + /// + internal static string InvalidRoleName + { + get { return GetString("InvalidRoleName"); } + } + + /// + /// Role name '{0}' is invalid. + /// + internal static string FormatInvalidRoleName(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidRoleName"), p0); + } + /// /// Invalid token. /// @@ -235,7 +267,7 @@ namespace Microsoft.AspNet.Identity } /// - /// User name {0} is invalid, can only contain letters or digits. + /// User name '{0}' is invalid, can only contain letters or digits. /// internal static string InvalidUserName { @@ -243,7 +275,7 @@ namespace Microsoft.AspNet.Identity } /// - /// User name {0} is invalid, can only contain letters or digits. + /// User name '{0}' is invalid, can only contain letters or digits. /// internal static string FormatInvalidUserName(object p0) { @@ -251,19 +283,19 @@ namespace Microsoft.AspNet.Identity } /// - /// Lockout is not enabled for this user. + /// A user with this login already exists. /// - internal static string LockoutNotEnabled + internal static string LoginAlreadyAssociated { - get { return GetString("LockoutNotEnabled"); } + get { return GetString("LoginAlreadyAssociated"); } } /// - /// Lockout is not enabled for this user. + /// A user with this login already exists. /// - internal static string FormatLockoutNotEnabled() + internal static string FormatLoginAlreadyAssociated() { - return GetString("LockoutNotEnabled"); + return GetString("LoginAlreadyAssociated"); } /// @@ -317,65 +349,65 @@ namespace Microsoft.AspNet.Identity /// /// Passwords must have at least one digit ('0'-'9'). /// - internal static string PasswordRequireDigit + internal static string PasswordRequiresDigit { - get { return GetString("PasswordRequireDigit"); } + get { return GetString("PasswordRequiresDigit"); } } /// /// Passwords must have at least one digit ('0'-'9'). /// - internal static string FormatPasswordRequireDigit() + internal static string FormatPasswordRequiresDigit() { - return GetString("PasswordRequireDigit"); + return GetString("PasswordRequiresDigit"); } /// /// Passwords must have at least one lowercase ('a'-'z'). /// - internal static string PasswordRequireLower + internal static string PasswordRequiresLower { - get { return GetString("PasswordRequireLower"); } + get { return GetString("PasswordRequiresLower"); } } /// /// Passwords must have at least one lowercase ('a'-'z'). /// - internal static string FormatPasswordRequireLower() + internal static string FormatPasswordRequiresLower() { - return GetString("PasswordRequireLower"); + return GetString("PasswordRequiresLower"); } /// /// Passwords must have at least one non letter and non digit character. /// - internal static string PasswordRequireNonLetterOrDigit + internal static string PasswordRequiresNonLetterAndDigit { - get { return GetString("PasswordRequireNonLetterOrDigit"); } + get { return GetString("PasswordRequiresNonLetterAndDigit"); } } /// /// Passwords must have at least one non letter and non digit character. /// - internal static string FormatPasswordRequireNonLetterOrDigit() + internal static string FormatPasswordRequiresNonLetterAndDigit() { - return GetString("PasswordRequireNonLetterOrDigit"); + return GetString("PasswordRequiresNonLetterAndDigit"); } /// /// Passwords must have at least one uppercase ('A'-'Z'). /// - internal static string PasswordRequireUpper + internal static string PasswordRequiresUpper { - get { return GetString("PasswordRequireUpper"); } + get { return GetString("PasswordRequiresUpper"); } } /// /// Passwords must have at least one uppercase ('A'-'Z'). /// - internal static string FormatPasswordRequireUpper() + internal static string FormatPasswordRequiresUpper() { - return GetString("PasswordRequireUpper"); + return GetString("PasswordRequiresUpper"); } /// @@ -394,22 +426,6 @@ namespace Microsoft.AspNet.Identity return string.Format(CultureInfo.CurrentCulture, GetString("PasswordTooShort"), p0); } - /// - /// {0} cannot be null or empty. - /// - internal static string PropertyTooShort - { - get { return GetString("PropertyTooShort"); } - } - - /// - /// {0} cannot be null or empty. - /// - internal static string FormatPropertyTooShort(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("PropertyTooShort"), p0); - } - /// /// Role {0} does not exist. /// @@ -651,7 +667,7 @@ namespace Microsoft.AspNet.Identity } /// - /// User already in role. + /// User already in role '{0}'. /// internal static string UserAlreadyInRole { @@ -659,11 +675,43 @@ namespace Microsoft.AspNet.Identity } /// - /// User already in role. + /// User already in role '{0}'. /// - internal static string FormatUserAlreadyInRole() + internal static string FormatUserAlreadyInRole(object p0) { - return GetString("UserAlreadyInRole"); + return string.Format(CultureInfo.CurrentCulture, GetString("UserAlreadyInRole"), p0); + } + + /// + /// User is locked out. + /// + internal static string UserLockedOut + { + get { return GetString("UserLockedOut"); } + } + + /// + /// User is locked out. + /// + internal static string FormatUserLockedOut() + { + return GetString("UserLockedOut"); + } + + /// + /// Lockout is not enabled for this user. + /// + internal static string UserLockoutNotEnabled + { + get { return GetString("UserLockoutNotEnabled"); } + } + + /// + /// Lockout is not enabled for this user. + /// + internal static string FormatUserLockoutNotEnabled() + { + return GetString("UserLockoutNotEnabled"); } /// @@ -683,7 +731,7 @@ namespace Microsoft.AspNet.Identity } /// - /// User is not in role. + /// User is not in role '{0}'. /// internal static string UserNotInRole { @@ -691,11 +739,91 @@ namespace Microsoft.AspNet.Identity } /// - /// User is not in role. + /// User is not in role '{0}'. /// - internal static string FormatUserNotInRole() + internal static string FormatUserNotInRole(object p0) { - return GetString("UserNotInRole"); + return string.Format(CultureInfo.CurrentCulture, GetString("UserNotInRole"), p0); + } + + /// + /// {0} : Failed : {1} + /// + internal static string LogIdentityResultFailure + { + get { return GetString("LogIdentityResultFailure"); } + } + + /// + /// {0} : Failed : {1} + /// + internal static string FormatLogIdentityResultFailure(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("LogIdentityResultFailure"), p0, p1); + } + + /// + /// {0} : Success + /// + internal static string LogIdentityResultSuccess + { + get { return GetString("LogIdentityResultSuccess"); } + } + + /// + /// {0} : Success + /// + internal static string FormatLogIdentityResultSuccess(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("LogIdentityResultSuccess"), p0); + } + + /// + /// {0} : Result : {1} + /// + internal static string LoggingSigninResult + { + get { return GetString("LoggingSigninResult"); } + } + + /// + /// {0} : Result : {1} + /// + internal static string FormatLoggingSigninResult(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("LoggingSigninResult"), p0, p1); + } + + /// + /// {0} for user: {1} + /// + internal static string LoggingResultMessage + { + get { return GetString("LoggingResultMessage"); } + } + + /// + /// {0} for user: {1} + /// + internal static string FormatLoggingResultMessage(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("LoggingResultMessage"), p0, p1); + } + + /// + /// {0} for role: {1} + /// + internal static string LoggingResultMessageForRole + { + get { return GetString("LoggingResultMessageForRole"); } + } + + /// + /// {0} for role: {1} + /// + internal static string FormatLoggingResultMessageForRole(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("LoggingResultMessageForRole"), p0, p1); } private static string GetString(string name, params string[] formatterNames) diff --git a/src/Microsoft.AspNet.Identity/Properties/debugSettings.json b/src/Microsoft.AspNet.Identity/Properties/debugSettings.json new file mode 100644 index 0000000000..a44fad34a3 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/Properties/debugSettings.json @@ -0,0 +1,3 @@ +{ + "Profiles": [] +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/Resources.resx b/src/Microsoft.AspNet.Identity/Resources.resx index 674ae53538..2351b1e7aa 100644 --- a/src/Microsoft.AspNet.Identity/Resources.resx +++ b/src/Microsoft.AspNet.Identity/Resources.resx @@ -117,6 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Optimistic concurrency failure, object has been modified. + Error when optimistic concurrency fails + Your security code is: {0} Default body format for the email @@ -126,7 +130,7 @@ Default name for the email token provider - Security Code + Security code Default subject for the email @@ -142,24 +146,24 @@ Default name for the phone number token provider - DefaultTokenProvider + Default Token Provider Name of the default token provider Email '{0}' is already taken. - error for duplicate emails + Error for duplicate emails - - Name {0} is already taken. - error for duplicate usernames + + Role name '{0}' is already taken. + Error for duplicate user names - - A user with that external login already exists. - Error when a login already linked + + User name '{0}' is already taken. + Error for duplicate user names Email '{0}' is invalid. - invalid email + Invalid email The provided PasswordHasherCompatibilityMode is invalid. @@ -169,17 +173,21 @@ The iteration count must be a positive integer. Error when the iteration count is < 1. + + Role name '{0}' is invalid. + Error for invalid role names + 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 + User name '{0}' is invalid, can only contain letters or digits. + User names can only contain letters or digits - - Lockout is not enabled for this user. - error when lockout is not enabled + + A user with this login already exists. + Error when a login already linked No IUserMessageProvider named '{0}' is registered. @@ -193,19 +201,19 @@ 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 and non 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 @@ -213,80 +221,104 @@ 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 + Error when a role does not exist Store does not implement IQueryableRoleStore<TRole>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IQueryableUserStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IRoleClaimStore<TRole>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserClaimStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserConfirmationStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserEmailStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserLockoutStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserLoginStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserPasswordStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserPhoneNumberStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserRoleStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserSecurityStampStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface Store does not implement IUserTwoFactorStore<TUser>. - error when the store does not implement this interface + Error when the store does not implement this interface User already has a password set. - error when AddPasswordAsync called when a user already has a password + Error when AddPasswordAsync called when a user already has a password - User already in role. + User already in role '{0}'. Error when a user is already in a role + + User is locked out. + Error when a user is locked out + + + Lockout is not enabled for this user. + Error when lockout is not enabled + User {0} does not exist. - error when a user does not exist + Error when a user does not exist - User is not in role. + User is not in role '{0}'. Error when a user is not in the role + + {0} : Failed : {1} + Logging method execution failure + + + {0} : Success + Logging method execution success + + + {0} : Result : {1} + Logging statement for SignInManager + + + {0} for user: {1} + Message prefix for Identity result + + + {0} for role: {1} + Message prefix for Identity result for role operation + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/RoleManager.cs b/src/Microsoft.AspNet.Identity/RoleManager.cs index 6b30e437fc..98b7db3990 100644 --- a/src/Microsoft.AspNet.Identity/RoleManager.cs +++ b/src/Microsoft.AspNet.Identity/RoleManager.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; -using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Identity { @@ -24,13 +24,19 @@ namespace Microsoft.AspNet.Identity /// /// The IRoleStore commits changes via the UpdateAsync/CreateAsync methods /// - public RoleManager(IRoleStore store, IEnumerable> roleValidators) + public RoleManager(IRoleStore store, + IEnumerable> roleValidators = null, + ILookupNormalizer keyNormalizer = null, + IdentityErrorDescriber errors = null, + ILoggerFactory loggerFactory = null) { if (store == null) { throw new ArgumentNullException("store"); } Store = store; + KeyNormalizer = keyNormalizer ?? new UpperInvariantLookupNormalizer(); + ErrorDescriber = errors ?? new IdentityErrorDescriber(); if (roleValidators != null) { @@ -39,6 +45,9 @@ namespace Microsoft.AspNet.Identity RoleValidators.Add(v); } } + + loggerFactory = loggerFactory ?? new LoggerFactory(); + Logger = loggerFactory.Create(nameof(RoleManager)); } /// @@ -51,6 +60,21 @@ namespace Microsoft.AspNet.Identity /// public IList> RoleValidators { get; } = new List>(); + /// + /// Used to generate public API error messages + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Used to log results + /// + public ILogger Logger { get; set; } + + /// + /// Used to normalize user names, role names, emails for uniqueness + /// + public ILookupNormalizer KeyNormalizer { get; set; } + /// /// Returns an IQueryable of roles if the store is an IQueryableRoleStore /// @@ -102,7 +126,7 @@ namespace Microsoft.AspNet.Identity private async Task ValidateRoleInternal(TRole role, CancellationToken cancellationToken) { - var errors = new List(); + var errors = new List(); foreach (var v in RoleValidators) { var result = await v.ValidateAsync(this, role, cancellationToken); @@ -120,7 +144,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task CreateAsync(TRole role, + public virtual async Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -134,17 +158,31 @@ namespace Microsoft.AspNet.Identity { return result; } - await Store.CreateAsync(role, cancellationToken); - return IdentityResult.Success; + await UpdateNormalizedRoleNameAsync(role, cancellationToken); + return await LogResultAsync(await Store.CreateAsync(role, cancellationToken), role); } + /// + /// Update the user's normalized user name + /// + /// + /// + /// + public virtual async Task UpdateNormalizedRoleNameAsync(TRole role, + CancellationToken cancellationToken = default(CancellationToken)) + { + var name = await GetRoleNameAsync(role, cancellationToken); + await Store.SetNormalizedRoleNameAsync(role, NormalizeKey(name), cancellationToken); + } + + /// /// UpdateAsync an existing role /// /// /// /// - public virtual async Task UpdateAsync(TRole role, + public virtual async Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -153,13 +191,19 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("role"); } + return await LogResultAsync(await UpdateRoleAsync(role, cancellationToken), role); + } + + private async Task UpdateRoleAsync(TRole role, + CancellationToken cancellationToken = default(CancellationToken)) + { var result = await ValidateRoleInternal(role, cancellationToken); if (!result.Succeeded) { return result; } - await Store.UpdateAsync(role, cancellationToken); - return IdentityResult.Success; + await UpdateNormalizedRoleNameAsync(role, cancellationToken); + return await Store.UpdateAsync(role, cancellationToken); } /// @@ -168,7 +212,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task DeleteAsync(TRole role, + public virtual async Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -176,9 +220,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("role"); } - - await Store.DeleteAsync(role, cancellationToken); - return IdentityResult.Success; + return await LogResultAsync(await Store.DeleteAsync(role, cancellationToken), role); } /// @@ -187,7 +229,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task RoleExistsAsync(string roleName, + public virtual async Task RoleExistsAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -196,16 +238,27 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("roleName"); } - return await FindByNameAsync(roleName, cancellationToken) != null; + return await FindByNameAsync(NormalizeKey(roleName), cancellationToken) != null; } + /// + /// Normalize a key (role name) for uniqueness comparisons + /// + /// + /// + public virtual string NormalizeKey(string key) + { + return (KeyNormalizer == null) ? key : KeyNormalizer.Normalize(key); + } + + /// /// FindByLoginAsync a role by id /// /// /// /// - public virtual async Task FindByIdAsync(string roleId, + public virtual async Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -218,7 +271,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task GetRoleNameAsync(TRole role, + public virtual async Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -232,12 +285,13 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task SetRoleNameAsync(TRole role, string name, + public virtual async Task SetRoleNameAsync(TRole role, string name, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); await Store.SetRoleNameAsync(role, name, cancellationToken); - return IdentityResult.Success; + await UpdateNormalizedRoleNameAsync(role, cancellationToken); + return await LogResultAsync(IdentityResult.Success, role); } /// @@ -246,7 +300,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task GetRoleIdAsync(TRole role, + public virtual async Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -259,7 +313,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task FindByNameAsync(string roleName, + public virtual async Task FindByNameAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -268,7 +322,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("roleName"); } - return await Store.FindByNameAsync(roleName, cancellationToken); + return await Store.FindByNameAsync(NormalizeKey(roleName), cancellationToken); } // IRoleClaimStore methods @@ -289,7 +343,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task AddClaimAsync(TRole role, Claim claim, + public virtual async Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -303,7 +357,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("role"); } await claimStore.AddClaimAsync(role, claim, cancellationToken); - return await UpdateAsync(role, cancellationToken); + return await LogResultAsync(await UpdateRoleAsync(role, cancellationToken), role); } /// @@ -313,7 +367,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task RemoveClaimAsync(TRole role, Claim claim, + public virtual async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -323,7 +377,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("role"); } await claimStore.RemoveClaimAsync(role, claim, cancellationToken); - return await UpdateAsync(role, cancellationToken); + return await LogResultAsync(await UpdateRoleAsync(role, cancellationToken), role); } /// @@ -332,7 +386,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task> GetClaimsAsync(TRole role, + public virtual async Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -344,6 +398,21 @@ namespace Microsoft.AspNet.Identity return await claimStore.GetClaimsAsync(role, cancellationToken); } + /// + /// Logs the current Identity Result and returns result object + /// + /// + /// + /// + /// + protected async Task LogResultAsync(IdentityResult result, + TRole role, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + result.Log(Logger, Resources.FormatLoggingResultMessageForRole(methodName, await GetRoleIdAsync(role))); + + return result; + } + private void ThrowIfDisposed() { if (_disposed) diff --git a/src/Microsoft.AspNet.Identity/RoleValidator.cs b/src/Microsoft.AspNet.Identity/RoleValidator.cs index 244b7136f0..fc20865ae9 100644 --- a/src/Microsoft.AspNet.Identity/RoleValidator.cs +++ b/src/Microsoft.AspNet.Identity/RoleValidator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Threading; using System.Threading.Tasks; @@ -15,6 +14,13 @@ namespace Microsoft.AspNet.Identity /// public class RoleValidator : IRoleValidator where TRole : class { + public RoleValidator(IdentityErrorDescriber errors = null) + { + Describer = errors ?? new IdentityErrorDescriber(); + } + + private IdentityErrorDescriber Describer { get; set; } + /// /// Validates a role before saving /// @@ -33,7 +39,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("role"); } - var errors = new List(); + var errors = new List(); await ValidateRoleName(manager, role, errors); if (errors.Count > 0) { @@ -42,13 +48,13 @@ namespace Microsoft.AspNet.Identity return IdentityResult.Success; } - private static async Task ValidateRoleName(RoleManager manager, TRole role, - ICollection errors) + private async Task ValidateRoleName(RoleManager manager, TRole role, + ICollection errors) { var roleName = await manager.GetRoleNameAsync(role); if (string.IsNullOrWhiteSpace(roleName)) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PropertyTooShort, "Name")); + errors.Add(Describer.InvalidRoleName(roleName)); } else { @@ -56,7 +62,7 @@ namespace Microsoft.AspNet.Identity if (owner != null && !string.Equals(await manager.GetRoleIdAsync(owner), await manager.GetRoleIdAsync(role))) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateName, roleName)); + errors.Add(Describer.DuplicateRoleName(roleName)); } } } diff --git a/src/Microsoft.AspNet.Identity/SignInManager.cs b/src/Microsoft.AspNet.Identity/SignInManager.cs index 846fd81540..d6fd276950 100644 --- a/src/Microsoft.AspNet.Identity/SignInManager.cs +++ b/src/Microsoft.AspNet.Identity/SignInManager.cs @@ -8,9 +8,10 @@ using System.Security.Claims; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; -using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Identity @@ -21,8 +22,11 @@ namespace Microsoft.AspNet.Identity /// public class SignInManager where TUser : class { - public SignInManager(UserManager userManager, IContextAccessor contextAccessor, - IClaimsIdentityFactory claimsFactory, IOptions optionsAccessor) + public SignInManager(UserManager userManager, + IHttpContextAccessor contextAccessor, + IClaimsIdentityFactory claimsFactory, + IOptions optionsAccessor = null, + ILoggerFactory loggerFactory = null) { if (userManager == null) { @@ -36,20 +40,21 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException(nameof(claimsFactory)); } - if (optionsAccessor == null || optionsAccessor.Options == null) - { - throw new ArgumentNullException(nameof(optionsAccessor)); - } + UserManager = userManager; Context = contextAccessor.Value; ClaimsFactory = claimsFactory; - Options = optionsAccessor.Options; + Options = optionsAccessor?.Options ?? new IdentityOptions(); + + loggerFactory = loggerFactory ?? new LoggerFactory(); + Logger = loggerFactory.Create(nameof(SignInManager)); } public UserManager UserManager { get; private set; } public HttpContext Context { get; private set; } public IClaimsIdentityFactory ClaimsFactory { get; private set; } public IdentityOptions Options { get; private set; } + public ILogger Logger { get; set; } // Should this be a func? public virtual async Task CreateUserIdentityAsync(TUser user, @@ -63,13 +68,13 @@ namespace Microsoft.AspNet.Identity { if (Options.SignIn.RequireConfirmedEmail && !(await UserManager.IsEmailConfirmedAsync(user, cancellationToken))) { - return false; + return await LogResultAsync(false, user); } if (Options.SignIn.RequireConfirmedPhoneNumber && !(await UserManager.IsPhoneNumberConfirmedAsync(user, cancellationToken))) { - return false; + return await LogResultAsync(false, user); } - return true; + return await LogResultAsync(true, user); } public virtual async Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null, @@ -96,15 +101,15 @@ namespace Microsoft.AspNet.Identity return UserManager.SupportsUserLockout && await UserManager.IsLockedOutAsync(user, token); } - private async Task PreSignInCheck(TUser user, CancellationToken token) + private async Task PreSignInCheck(TUser user, CancellationToken token) { if (!await CanSignInAsync(user, token)) { - return SignInStatus.NotAllowed; + return SignInResult.NotAllowed; } if (await IsLockedOut(user, token)) { - return SignInStatus.LockedOut; + return SignInResult.LockedOut; } return null; } @@ -141,18 +146,26 @@ namespace Microsoft.AspNet.Identity return null; } - public virtual async Task PasswordSignInAsync(TUser user, string password, + public virtual async Task PasswordSignInAsync(TUser user, string password, bool isPersistent, bool shouldLockout, CancellationToken cancellationToken = default(CancellationToken)) { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } var error = await PreSignInCheck(user, cancellationToken); if (error != null) { - return error.Value; + return await LogResultAsync(error, user); + } + if (await IsLockedOut(user, cancellationToken)) + { + return await LogResultAsync(SignInResult.LockedOut, user); } if (await UserManager.CheckPasswordAsync(user, password, cancellationToken)) { await ResetLockout(user, cancellationToken); - return await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken); + return await LogResultAsync(await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken), user); } if (UserManager.SupportsUserLockout && shouldLockout) { @@ -160,19 +173,20 @@ namespace Microsoft.AspNet.Identity await UserManager.AccessFailedAsync(user, cancellationToken); if (await UserManager.IsLockedOutAsync(user, cancellationToken)) { - return SignInStatus.LockedOut; + + return await LogResultAsync(SignInResult.LockedOut, user); } } - return SignInStatus.Failure; + return await LogResultAsync(SignInResult.Failed, user); } - public virtual async Task PasswordSignInAsync(string userName, string password, + public virtual async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout, CancellationToken cancellationToken = default(CancellationToken)) { var user = await UserManager.FindByNameAsync(userName, cancellationToken); if (user == null) { - return SignInStatus.Failure; + return SignInResult.Failed; } return await PasswordSignInAsync(user, password, isPersistent, shouldLockout, cancellationToken); } @@ -207,9 +221,8 @@ namespace Microsoft.AspNet.Identity return false; } var token = await UserManager.GenerateTwoFactorTokenAsync(user, provider, cancellationToken); - // See IdentityConfig.cs to plug in Email/SMS services to actually send the code await UserManager.NotifyTwoFactorTokenAsync(user, provider, token, cancellationToken); - return true; + return await LogResultAsync(true, user); } public async Task IsTwoFactorClientRememberedAsync(TUser user, @@ -236,23 +249,23 @@ namespace Microsoft.AspNet.Identity return Task.FromResult(0); } - public virtual async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, + public virtual async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient, CancellationToken cancellationToken = default(CancellationToken)) { var twoFactorInfo = await RetrieveTwoFactorInfoAsync(cancellationToken); if (twoFactorInfo == null || twoFactorInfo.UserId == null) { - return SignInStatus.Failure; + return SignInResult.Failed; } var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId, cancellationToken); if (user == null) { - return SignInStatus.Failure; + return SignInResult.Failed; } var error = await PreSignInCheck(user, cancellationToken); if (error != null) { - return error.Value; + return await LogResultAsync(error, user); } if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code, cancellationToken)) { @@ -268,11 +281,13 @@ namespace Microsoft.AspNet.Identity { await RememberTwoFactorClientAsync(user, cancellationToken); } - return SignInStatus.Success; + await UserManager.ResetAccessFailedCountAsync(user, cancellationToken); + await SignInAsync(user, isPersistent); + return await LogResultAsync(SignInResult.Success, user); } // If the token is incorrect, record the failure which also may cause the user to be locked out await UserManager.AccessFailedAsync(user, cancellationToken); - return SignInStatus.Failure; + return await LogResultAsync(SignInResult.Failed, user); } /// @@ -292,20 +307,20 @@ namespace Microsoft.AspNet.Identity return await UserManager.FindByIdAsync(info.UserId, cancellationToken); } - public virtual async Task ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, + public async Task ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, CancellationToken cancellationToken = default(CancellationToken)) { var user = await UserManager.FindByLoginAsync(loginProvider, providerKey, cancellationToken); if (user == null) { - return SignInStatus.Failure; + return SignInResult.Failed; } var error = await PreSignInCheck(user, cancellationToken); if (error != null) { - return error.Value; + return await LogResultAsync(error, user); } - return await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken, loginProvider); + return await LogResultAsync(await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken, loginProvider), user); } private const string LoginProviderKey = "LoginProvider"; @@ -316,7 +331,7 @@ namespace Microsoft.AspNet.Identity return Context.GetAuthenticationTypes().Where(d => !string.IsNullOrEmpty(d.Caption)); } - public virtual async Task GetExternalLoginInfoAsync(string expectedXsrf = null, + public virtual async Task GetExternalLoginInfoAsync(string expectedXsrf = null, CancellationToken cancellationToken = default(CancellationToken)) { var auth = await Context.AuthenticateAsync(IdentityOptions.ExternalCookieAuthenticationType); @@ -358,10 +373,10 @@ namespace Microsoft.AspNet.Identity return properties; } - private async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, + private async Task SignInOrTwoFactorAsync(TUser user, bool isPersistent, CancellationToken cancellationToken, string loginProvider = null) { - if (UserManager.SupportsUserTwoFactor && + if (UserManager.SupportsUserTwoFactor && await UserManager.GetTwoFactorEnabledAsync(user, cancellationToken) && (await UserManager.GetValidTwoFactorProvidersAsync(user, cancellationToken)).Count > 0) { @@ -370,7 +385,7 @@ namespace Microsoft.AspNet.Identity // Store the userId for use after two factor check var userId = await UserManager.GetUserIdAsync(user, cancellationToken); Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider)); - return SignInStatus.RequiresVerification; + return SignInResult.TwoFactorRequired; } } // Cleanup external cookie @@ -379,7 +394,7 @@ namespace Microsoft.AspNet.Identity Context.Response.SignOut(IdentityOptions.ExternalCookieAuthenticationType); } await SignInAsync(user, isPersistent, loginProvider, cancellationToken); - return SignInStatus.Success; + return SignInResult.Success; } private async Task RetrieveTwoFactorInfoAsync(CancellationToken cancellationToken) @@ -396,6 +411,35 @@ namespace Microsoft.AspNet.Identity return null; } + /// + /// Log boolean result for user and return result + /// + /// + /// + /// + /// + protected async virtual Task LogResultAsync(bool result, TUser user, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + Logger.WriteInformation(Resources.FormatLoggingSigninResult(Resources.FormatLoggingResultMessage(methodName, + await UserManager.GetUserIdAsync(user)), result)); + + return result; + } + + /// + /// Log SignInStatus for user and return SignInStatus + /// + /// + /// + /// + /// + protected async virtual Task LogResultAsync(SignInResult status, TUser user, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + status.Log(Logger, Resources.FormatLoggingResultMessage(methodName, await UserManager.GetUserIdAsync(user))); + + return status; + } + internal static ClaimsIdentity StoreTwoFactorInfo(string userId, string loginProvider) { var identity = new ClaimsIdentity(IdentityOptions.TwoFactorUserIdCookieAuthenticationType); diff --git a/src/Microsoft.AspNet.Identity/SignInResult.cs b/src/Microsoft.AspNet.Identity/SignInResult.cs new file mode 100644 index 0000000000..7ef69a60bb --- /dev/null +++ b/src/Microsoft.AspNet.Identity/SignInResult.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Represents the result of an sign in operation + /// + public class SignInResult + { + private static readonly SignInResult _success = new SignInResult { Succeeded = true }; + private static readonly SignInResult _failed = new SignInResult(); + private static readonly SignInResult _lockedOut = new SignInResult { IsLockedOut = true }; + private static readonly SignInResult _notAllowed = new SignInResult { IsNotAllowed = true }; + private static readonly SignInResult _twoFactorRequired = new SignInResult { RequiresTwoFactor = true }; + + /// + /// True if the operation was successful + /// + public bool Succeeded { get; protected set; } + + /// + /// True if the user is locked out + /// + public bool IsLockedOut { get; protected set; } + + /// + /// True if the user is not allowed to sign in + /// + public bool IsNotAllowed { get; protected set; } + + /// + /// True if the sign in requires two factor + /// + public bool RequiresTwoFactor { get; protected set; } + + /// + /// Static success result + /// + /// + public static SignInResult Success + { + get { return _success; } + } + + /// + /// Static failure result + /// + /// + public static SignInResult Failed + { + get { return _failed; } + } + + /// + /// Static locked out result + /// + /// + public static SignInResult LockedOut + { + get { return _lockedOut; } + } + + /// + /// Static not allowed result + /// + /// + public static SignInResult NotAllowed + { + get { return _notAllowed; } + } + + /// + /// Static two factor required result + /// + /// + public static SignInResult TwoFactorRequired + { + get { return _twoFactorRequired; } + } + + /// + /// Log result based on properties + /// + /// + /// + public virtual void Log(ILogger logger, string message) + { + if (IsLockedOut) + { + logger.WriteInformation(Resources.FormatLoggingSigninResult(message, "Lockedout")); + } + else if (IsNotAllowed) + { + logger.WriteInformation(Resources.FormatLoggingSigninResult(message, "NotAllowed")); + } + else if (RequiresTwoFactor) + { + logger.WriteInformation(Resources.FormatLoggingSigninResult(message, "RequiresTwoFactor")); + } + else if (Succeeded) + { + logger.WriteInformation(Resources.FormatLoggingSigninResult(message, "Succeeded")); + } + else + { + logger.WriteInformation(Resources.FormatLoggingSigninResult(message, "Failed")); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UpperInvariantUserNameNormalizer.cs b/src/Microsoft.AspNet.Identity/UpperInvariantLookupNormalizer.cs similarity index 54% rename from src/Microsoft.AspNet.Identity/UpperInvariantUserNameNormalizer.cs rename to src/Microsoft.AspNet.Identity/UpperInvariantLookupNormalizer.cs index 1564ecd3d4..9bfa9454a3 100644 --- a/src/Microsoft.AspNet.Identity/UpperInvariantUserNameNormalizer.cs +++ b/src/Microsoft.AspNet.Identity/UpperInvariantLookupNormalizer.cs @@ -6,22 +6,22 @@ using System; namespace Microsoft.AspNet.Identity { /// - /// Normalizes user names via ToUpperInvariant() + /// Normalizes via ToUpperInvariant() /// - public class UpperInvariantUserNameNormalizer : IUserNameNormalizer + public class UpperInvariantLookupNormalizer : ILookupNormalizer { /// - /// Normalizes user names via ToUpperInvariant() + /// Normalizes via ToUpperInvariant() /// - /// + /// /// - public string Normalize(string userName) + public string Normalize(string key) { - if (userName == null) + if (key == null) { return null; } - return userName.Normalize().ToUpperInvariant(); + return key.Normalize().ToUpperInvariant(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index 3b18d534fd..fa63e7d5ff 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Identity @@ -30,39 +31,39 @@ namespace Microsoft.AspNet.Identity private IdentityOptions _options; /// - /// Constructor which takes a service provider and user store + /// Constructor /// /// /// /// - /// - /// - /// - public UserManager(IUserStore store, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - IUserNameNormalizer userNameNormalizer, - IEnumerable> tokenProviders, - IEnumerable msgProviders) + /// + /// + /// + /// + /// + /// + /// + public UserManager(IUserStore store, + IOptions optionsAccessor = null, + IPasswordHasher passwordHasher = null, + IEnumerable> userValidators = null, + IEnumerable> passwordValidators = null, + ILookupNormalizer keyNormalizer = null, + IdentityErrorDescriber errors = null, + IEnumerable> tokenProviders = null, + IEnumerable msgProviders = null, + ILoggerFactory loggerFactory = null) { if (store == null) { throw new ArgumentNullException(nameof(store)); } - if (optionsAccessor == null || optionsAccessor.Options == null) - { - throw new ArgumentNullException(nameof(optionsAccessor)); - } - if (passwordHasher == null) - { - throw new ArgumentNullException(nameof(passwordHasher)); - } Store = store; - Options = optionsAccessor.Options; - PasswordHasher = passwordHasher; - UserNameNormalizer = userNameNormalizer; + Options = optionsAccessor?.Options ?? new IdentityOptions(); + PasswordHasher = passwordHasher ?? new PasswordHasher(); + KeyNormalizer = keyNormalizer ?? new UpperInvariantLookupNormalizer(); + ErrorDescriber = errors ?? new IdentityErrorDescriber(); + if (userValidators != null) { foreach (var v in userValidators) @@ -77,6 +78,10 @@ namespace Microsoft.AspNet.Identity PasswordValidators.Add(v); } } + + loggerFactory = loggerFactory ?? new LoggerFactory(); + Logger = loggerFactory.Create(nameof(UserManager)); + if (tokenProviders != null) { foreach (var tokenProvider in tokenProviders) @@ -132,9 +137,19 @@ namespace Microsoft.AspNet.Identity public IList> PasswordValidators { get; } = new List>(); /// - /// Used to normalize user names for uniqueness + /// Used to normalize user names and emails for uniqueness /// - public IUserNameNormalizer UserNameNormalizer { get; set; } + public ILookupNormalizer KeyNormalizer { get; set; } + + /// + /// Used to generate public API error messages + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Used to log IdentityResult + /// + public ILogger Logger { get; set; } public IdentityOptions Options { @@ -301,7 +316,7 @@ namespace Microsoft.AspNet.Identity private async Task ValidateUserInternal(TUser user, CancellationToken cancellationToken) { - var errors = new List(); + var errors = new List(); foreach (var v in UserValidators) { var result = await v.ValidateAsync(this, user, cancellationToken); @@ -315,7 +330,7 @@ namespace Microsoft.AspNet.Identity private async Task ValidatePasswordInternal(TUser user, string password, CancellationToken cancellationToken) { - var errors = new List(); + var errors = new List(); foreach (var v in PasswordValidators) { var result = await v.ValidateAsync(this, user, password, cancellationToken); @@ -327,6 +342,31 @@ namespace Microsoft.AspNet.Identity return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; } + public virtual Task GenerateConcurrencyStampAsync(TUser user, + CancellationToken token = default(CancellationToken)) + { + return Task.FromResult(Guid.NewGuid().ToString()); + } + + /// + /// Validate user and update. Called by other UserManager methods + /// + /// + /// + /// + private async Task UpdateUserAsync(TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + var result = await ValidateUserInternal(user, cancellationToken); + if (!result.Succeeded) + { + return result; + } + await UpdateNormalizedUserNameAsync(user, cancellationToken); + await UpdateNormalizedEmailAsync(user, cancellationToken); + return await Store.UpdateAsync(user, cancellationToken); + } + /// /// Create a user with no password /// @@ -348,8 +388,8 @@ namespace Microsoft.AspNet.Identity await GetUserLockoutStore().SetLockoutEnabledAsync(user, true, cancellationToken); } await UpdateNormalizedUserNameAsync(user, cancellationToken); - await Store.CreateAsync(user, cancellationToken); - return IdentityResult.Success; + await UpdateNormalizedEmailAsync(user, cancellationToken); + return await LogResultAsync(await Store.CreateAsync(user, cancellationToken), user); } /// @@ -366,14 +406,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - var result = await ValidateUserInternal(user, cancellationToken); - if (!result.Succeeded) - { - return result; - } - await UpdateNormalizedUserNameAsync(user, cancellationToken); - await Store.UpdateAsync(user, cancellationToken); - return IdentityResult.Success; + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -390,8 +423,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - await Store.DeleteAsync(user, cancellationToken); - return IdentityResult.Success; + return await LogResultAsync(await Store.DeleteAsync(user, cancellationToken), user); } /// @@ -421,7 +453,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("userName"); } - userName = NormalizeUserName(userName); + userName = NormalizeKey(userName); return Store.FindByNameAsync(userName, cancellationToken); } @@ -456,7 +488,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("password"); } - var result = await UpdatePasswordInternal(passwordStore, user, password, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, password, cancellationToken); if (!result.Succeeded) { return result; @@ -465,13 +497,13 @@ namespace Microsoft.AspNet.Identity } /// - /// Normalize a user name for uniqueness comparisons + /// Normalize a key (user name, email) for uniqueness comparisons /// /// /// - public virtual string NormalizeUserName(string userName) + public virtual string NormalizeKey(string key) { - return (UserNameNormalizer == null) ? userName : UserNameNormalizer.Normalize(userName); + return (KeyNormalizer == null) ? key : KeyNormalizer.Normalize(key); } /// @@ -483,8 +515,8 @@ namespace Microsoft.AspNet.Identity public virtual async Task UpdateNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { - var userName = await GetUserNameAsync(user, cancellationToken); - await Store.SetNormalizedUserNameAsync(user, NormalizeUserName(userName), cancellationToken); + var normalizedName = NormalizeKey(await GetUserNameAsync(user, cancellationToken)); + await Store.SetNormalizedUserNameAsync(user, normalizedName, cancellationToken); } /// @@ -520,7 +552,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } await UpdateUserName(user, userName, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } private async Task UpdateUserName(TUser user, string userName, CancellationToken cancellationToken) @@ -558,7 +590,14 @@ namespace Microsoft.AspNet.Identity { return false; } - return await VerifyPasswordAsync(passwordStore, user, password, cancellationToken); + var result = await VerifyPasswordAsync(passwordStore, user, password, cancellationToken); + if (result == PasswordVerificationResult.SuccessRehashNeeded) + { + await UpdatePasswordHash(passwordStore, user, password, cancellationToken, validatePassword: false); + await UpdateUserAsync(user, cancellationToken); + } + + return await LogResultAsync(result != PasswordVerificationResult.Failed, user); } /// @@ -576,7 +615,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - return await passwordStore.HasPasswordAsync(user, cancellationToken); + return await LogResultAsync(await passwordStore.HasPasswordAsync(user, cancellationToken), user); } /// @@ -598,14 +637,14 @@ namespace Microsoft.AspNet.Identity var hash = await passwordStore.GetPasswordHashAsync(user, cancellationToken); if (hash != null) { - return new IdentityResult(Resources.UserAlreadyHasPassword); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.UserAlreadyHasPassword()), user); } - var result = await UpdatePasswordInternal(passwordStore, user, password, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, password, cancellationToken); if (!result.Succeeded) { - return result; + return await LogResultAsync(result, user); } - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -625,16 +664,16 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - if (await VerifyPasswordAsync(passwordStore, user, currentPassword, cancellationToken)) + if (await VerifyPasswordAsync(passwordStore, user, currentPassword, cancellationToken) != PasswordVerificationResult.Failed) { - var result = await UpdatePasswordInternal(passwordStore, user, newPassword, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, newPassword, cancellationToken); if (!result.Succeeded) { - return result; + return await LogResultAsync(result, user); } - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } - return IdentityResult.Failed(Resources.PasswordMismatch); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.PasswordMismatch()), user); } /// @@ -652,21 +691,24 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - await passwordStore.SetPasswordHashAsync(user, null, cancellationToken); - await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + await UpdatePasswordHash(passwordStore, user, null, cancellationToken, validatePassword: false); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } - internal async Task UpdatePasswordInternal(IUserPasswordStore passwordStore, - TUser user, string newPassword, CancellationToken cancellationToken) + internal async Task UpdatePasswordHash(IUserPasswordStore passwordStore, + TUser user, string newPassword, CancellationToken cancellationToken, bool validatePassword = true) { - var validate = await ValidatePasswordInternal(user, newPassword, cancellationToken); - if (!validate.Succeeded) + if (validatePassword) { - return validate; + var validate = await ValidatePasswordInternal(user, newPassword, cancellationToken); + if (!validate.Succeeded) + { + return validate; + } } + var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null; await - passwordStore.SetPasswordHashAsync(user, PasswordHasher.HashPassword(user, newPassword), cancellationToken); + passwordStore.SetPasswordHashAsync(user, hash, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); return IdentityResult.Success; } @@ -679,11 +721,11 @@ namespace Microsoft.AspNet.Identity /// /// /// - protected virtual async Task VerifyPasswordAsync(IUserPasswordStore store, TUser user, + protected virtual async Task VerifyPasswordAsync(IUserPasswordStore store, TUser user, string password, CancellationToken cancellationToken = default(CancellationToken)) { var hash = await store.GetPasswordHashAsync(user, cancellationToken); - return PasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed; + return PasswordHasher.VerifyHashedPassword(user, hash, password); } // IUserSecurityStampStore methods @@ -731,7 +773,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -744,7 +786,10 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - return await GenerateUserTokenAsync(user, Options.PasswordResetTokenProvider, "ResetPassword", cancellationToken); + var token = await GenerateUserTokenAsync(user, Options.PasswordResetTokenProvider, "ResetPassword", cancellationToken); + await LogResultAsync(IdentityResult.Success, user); + + return token; } /// @@ -766,15 +811,15 @@ namespace Microsoft.AspNet.Identity // Make sure the token is valid and the stamp matches if (!await VerifyUserTokenAsync(user, Options.PasswordResetTokenProvider, "ResetPassword", token, cancellationToken)) { - return IdentityResult.Failed(Resources.InvalidToken); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); } var passwordStore = GetPasswordStore(); - var result = await UpdatePasswordInternal(passwordStore, user, newPassword, cancellationToken); + var result = await UpdatePasswordHash(passwordStore, user, newPassword, cancellationToken); if (!result.Succeeded) { - return result; + return await LogResultAsync(result, user); } - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } // Update the security stamp if the store supports it @@ -851,7 +896,7 @@ namespace Microsoft.AspNet.Identity } await loginStore.RemoveLoginAsync(user, loginProvider, providerKey, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -877,10 +922,10 @@ namespace Microsoft.AspNet.Identity var existingUser = await FindByLoginAsync(login.LoginProvider, login.ProviderKey, cancellationToken); if (existingUser != null) { - return IdentityResult.Failed(Resources.ExternalLoginExists); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.LoginAlreadyAssociated()), user); } await loginStore.AddLoginAsync(user, login, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -956,7 +1001,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } await claimStore.AddClaimsAsync(user, claims, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -985,7 +1030,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } await claimStore.ReplaceClaimAsync(user, claim, newClaim, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1032,7 +1077,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("claims"); } await claimStore.RemoveClaimsAsync(user, claims, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1082,10 +1127,10 @@ namespace Microsoft.AspNet.Identity var userRoles = await userRoleStore.GetRolesAsync(user, cancellationToken); if (userRoles.Contains(role)) { - return new IdentityResult(Resources.UserAlreadyInRole); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.UserAlreadyInRole(role)), user); } await userRoleStore.AddToRoleAsync(user, role, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1113,11 +1158,11 @@ namespace Microsoft.AspNet.Identity { if (userRoles.Contains(role)) { - return new IdentityResult(Resources.UserAlreadyInRole); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.UserAlreadyInRole(role)), user); } await userRoleStore.AddToRoleAsync(user, role, cancellationToken); } - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1138,10 +1183,10 @@ namespace Microsoft.AspNet.Identity } if (!await userRoleStore.IsInRoleAsync(user, role, cancellationToken)) { - return new IdentityResult(Resources.UserNotInRole); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.UserNotInRole(role)), user); } await userRoleStore.RemoveFromRoleAsync(user, role, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1168,11 +1213,11 @@ namespace Microsoft.AspNet.Identity { if (!await userRoleStore.IsInRoleAsync(user, role, cancellationToken)) { - return new IdentityResult(Resources.UserNotInRole); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.UserNotInRole(role)), user); } await userRoleStore.RemoveFromRoleAsync(user, role, cancellationToken); } - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1213,10 +1258,10 @@ namespace Microsoft.AspNet.Identity } // IUserEmailStore methods - internal IUserEmailStore GetEmailStore() + internal IUserEmailStore GetEmailStore(bool throwOnFail = true) { var cast = Store as IUserEmailStore; - if (cast == null) + if (throwOnFail && cast == null) { throw new NotSupportedException(Resources.StoreNotIUserEmailStore); } @@ -1261,7 +1306,7 @@ namespace Microsoft.AspNet.Identity await store.SetEmailAsync(user, email, cancellationToken); await store.SetEmailConfirmedAsync(user, false, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1279,20 +1324,41 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("email"); } - return store.FindByEmailAsync(email, cancellationToken); + return store.FindByEmailAsync(NormalizeKey(email), cancellationToken); } + /// + /// Update the user's normalized email + /// + /// + /// + /// + public virtual async Task UpdateNormalizedEmailAsync(TUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + var store = GetEmailStore(throwOnFail: false); + if (store != null) + { + var email = await GetEmailAsync(user, cancellationToken); + await store.SetNormalizedEmailAsync(user, NormalizeKey(email), cancellationToken); + } + } + + /// /// Get the confirmation token for the user /// /// /// /// - public virtual Task GenerateEmailConfirmationTokenAsync(TUser user, + public async virtual Task GenerateEmailConfirmationTokenAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - return GenerateUserTokenAsync(user, Options.EmailConfirmationTokenProvider, "Confirmation", cancellationToken); + var token = await GenerateUserTokenAsync(user, Options.EmailConfirmationTokenProvider, "Confirmation", cancellationToken); + await LogResultAsync(IdentityResult.Success, user); + + return token; } /// @@ -1313,10 +1379,10 @@ namespace Microsoft.AspNet.Identity } if (!await VerifyUserTokenAsync(user, Options.EmailConfirmationTokenProvider, "Confirmation", token, cancellationToken)) { - return IdentityResult.Failed(Resources.InvalidToken); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); } await store.SetEmailConfirmedAsync(user, true, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1352,7 +1418,10 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - return await GenerateUserTokenAsync(user, Options.ChangeEmailTokenProvider, GetChangeEmailPurpose(newEmail), cancellationToken); + var token = await GenerateUserTokenAsync(user, Options.ChangeEmailTokenProvider, GetChangeEmailPurpose(newEmail), cancellationToken); + await LogResultAsync(IdentityResult.Success, user); + + return token; } /// @@ -1374,13 +1443,13 @@ namespace Microsoft.AspNet.Identity // Make sure the token is valid and the stamp matches if (!await VerifyUserTokenAsync(user, Options.ChangeEmailTokenProvider, GetChangeEmailPurpose(newEmail), token, cancellationToken)) { - return IdentityResult.Failed(Resources.InvalidToken); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); } var store = GetEmailStore(); await store.SetEmailAsync(user, newEmail, cancellationToken); await store.SetEmailConfirmedAsync(user, true, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } // IUserPhoneNumberStore methods @@ -1431,7 +1500,7 @@ namespace Microsoft.AspNet.Identity await store.SetPhoneNumberAsync(user, phoneNumber, cancellationToken); await store.SetPhoneNumberConfirmedAsync(user, false, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1453,12 +1522,12 @@ namespace Microsoft.AspNet.Identity } if (!await VerifyChangePhoneNumberTokenAsync(user, token, phoneNumber, cancellationToken)) { - return IdentityResult.Failed(Resources.InvalidToken); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); } await store.SetPhoneNumberAsync(user, phoneNumber, cancellationToken); await store.SetPhoneNumberConfirmedAsync(user, true, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1497,9 +1566,12 @@ namespace Microsoft.AspNet.Identity CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); - return Rfc6238AuthenticationService.GenerateCode( + var token = Rfc6238AuthenticationService.GenerateCode( await CreateSecurityTokenAsync(user, cancellationToken), phoneNumber) .ToString(CultureInfo.InvariantCulture); + + await LogResultAsync(IdentityResult.Success, user); + return token; } /// @@ -1517,8 +1589,13 @@ namespace Microsoft.AspNet.Identity int code; if (securityToken != null && Int32.TryParse(token, out code)) { - return Rfc6238AuthenticationService.ValidateCode(securityToken, code, phoneNumber); + if (Rfc6238AuthenticationService.ValidateCode(securityToken, code, phoneNumber)) + { + await LogResultAsync(IdentityResult.Success, user); + return true; + } } + await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); return false; } @@ -1547,7 +1624,18 @@ namespace Microsoft.AspNet.Identity throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.NoTokenProvider, tokenProvider)); } // Make sure the token is valid - return await _tokenProviders[tokenProvider].ValidateAsync(purpose, token, this, user, cancellationToken); + var result = await _tokenProviders[tokenProvider].ValidateAsync(purpose, token, this, user, cancellationToken); + + if (result) + { + await LogResultAsync(IdentityResult.Success, user); + } + else + { + await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); + } + + return result; } /// @@ -1573,7 +1661,11 @@ namespace Microsoft.AspNet.Identity { throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.NoTokenProvider, tokenProvider)); } - return await _tokenProviders[tokenProvider].GenerateAsync(purpose, this, user, cancellationToken); + + var token = await _tokenProviders[tokenProvider].GenerateAsync(purpose, this, user, cancellationToken); + await LogResultAsync(IdentityResult.Success, user); + + return token; } /// @@ -1651,7 +1743,18 @@ namespace Microsoft.AspNet.Identity Resources.NoTokenProvider, tokenProvider)); } // Make sure the token is valid - return await _tokenProviders[tokenProvider].ValidateAsync("TwoFactor", token, this, user, cancellationToken); + var result = await _tokenProviders[tokenProvider].ValidateAsync("TwoFactor", token, this, user, cancellationToken); + + if (result) + { + await LogResultAsync(IdentityResult.Success, user); + } + else + { + await LogResultAsync(IdentityResult.Failed(ErrorDescriber.InvalidToken()), user); + } + + return result; } /// @@ -1674,7 +1777,10 @@ namespace Microsoft.AspNet.Identity throw new NotSupportedException(String.Format(CultureInfo.CurrentCulture, Resources.NoTokenProvider, tokenProvider)); } - return await _tokenProviders[tokenProvider].GenerateAsync("TwoFactor", this, user, cancellationToken); + var token = await _tokenProviders[tokenProvider].GenerateAsync("TwoFactor", this, user, cancellationToken); + await LogResultAsync(IdentityResult.Success, user); + + return token; } /// @@ -1703,7 +1809,7 @@ namespace Microsoft.AspNet.Identity Resources.NoTokenProvider, tokenProvider)); } await _tokenProviders[tokenProvider].NotifyAsync(token, this, user, cancellationToken); - return IdentityResult.Success; + return await LogResultAsync(IdentityResult.Success, user); } // IUserFactorStore methods @@ -1753,7 +1859,7 @@ namespace Microsoft.AspNet.Identity } await store.SetTwoFactorEnabledAsync(user, enabled, cancellationToken); await UpdateSecurityStampInternal(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } // Messaging methods @@ -1765,7 +1871,7 @@ namespace Microsoft.AspNet.Identity /// /// /// - public virtual async Task SendMessageAsync(string messageProvider, IdentityMessage message, + public virtual async Task SendMessageAsync(string messageProvider, IdentityMessage message, CancellationToken cancellationToken = default(CancellationToken)) { ThrowIfDisposed(); @@ -1833,7 +1939,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } await store.SetLockoutEnabledAsync(user, enabled, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1890,10 +1996,10 @@ namespace Microsoft.AspNet.Identity } if (!await store.GetLockoutEnabledAsync(user, cancellationToken).ConfigureAwait((false))) { - return IdentityResult.Failed(Resources.LockoutNotEnabled); + return await LogResultAsync(IdentityResult.Failed(ErrorDescriber.UserLockoutNotEnabled()), user); } await store.SetLockoutEndDateAsync(user, lockoutEnd, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1917,12 +2023,12 @@ namespace Microsoft.AspNet.Identity var count = await store.IncrementAccessFailedCountAsync(user, cancellationToken); if (count < Options.Lockout.MaxFailedAccessAttempts) { - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } await store.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan), cancellationToken); await store.ResetAccessFailedCountAsync(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1941,7 +2047,7 @@ namespace Microsoft.AspNet.Identity throw new ArgumentNullException("user"); } await store.ResetAccessFailedCountAsync(user, cancellationToken); - return await UpdateAsync(user, cancellationToken); + return await LogResultAsync(await UpdateUserAsync(user, cancellationToken), user); } /// @@ -1962,6 +2068,75 @@ namespace Microsoft.AspNet.Identity return await store.GetAccessFailedCountAsync(user, cancellationToken); } + public virtual Task> GetUsersForClaimAsync(Claim claim, + CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + var store = GetClaimStore(); + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + return store.GetUsersForClaimAsync(claim, cancellationToken); + } + + /// + /// Get all the users in a role + /// + /// + /// + /// + public virtual Task> GetUsersInRoleAsync(string roleName, + CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + var store = GetUserRoleStore(); + if (roleName == null) + { + throw new ArgumentNullException("role"); + } + + return store.GetUsersInRoleAsync(roleName, cancellationToken); + } + + /// + /// Logs the current Identity Result and returns result object + /// + /// + /// + /// + /// + protected async Task LogResultAsync(IdentityResult result, + TUser user, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + result.Log(Logger, Resources.FormatLoggingResultMessage(methodName, await GetUserIdAsync(user))); + + return result; + } + + /// + /// Logs result of operation being true/false + /// + /// + /// + /// + /// result + protected async Task LogResultAsync(bool result, + TUser user, [System.Runtime.CompilerServices.CallerMemberName] string methodName = "") + { + var baseMessage = Resources.FormatLoggingResultMessage(methodName, await GetUserIdAsync(user)); + if (result) + { + Logger.WriteInformation(string.Format("{0} : {1}", baseMessage, result.ToString())); + } + else + { + Logger.WriteWarning(string.Format("{0} : {1}", baseMessage, result.ToString())); + } + + return result; + } + private void ThrowIfDisposed() { if (_disposed) diff --git a/src/Microsoft.AspNet.Identity/UserValidator.cs b/src/Microsoft.AspNet.Identity/UserValidator.cs index 749f3a9f3f..06ab306683 100644 --- a/src/Microsoft.AspNet.Identity/UserValidator.cs +++ b/src/Microsoft.AspNet.Identity/UserValidator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; #if ASPNET50 using System.Net.Mail; #endif @@ -19,6 +18,13 @@ namespace Microsoft.AspNet.Identity /// public class UserValidator : IUserValidator where TUser : class { + public UserValidator(IdentityErrorDescriber errors = null) + { + Describer = errors ?? new IdentityErrorDescriber(); + } + + public IdentityErrorDescriber Describer { get; private set; } + /// /// Validates a user before saving /// @@ -37,7 +43,7 @@ namespace Microsoft.AspNet.Identity { throw new ArgumentNullException("user"); } - var errors = new List(); + var errors = new List(); await ValidateUserName(manager, user, errors); if (manager.Options.User.RequireUniqueEmail) { @@ -46,16 +52,16 @@ namespace Microsoft.AspNet.Identity return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; } - private async Task ValidateUserName(UserManager manager, TUser user, ICollection errors) + private async Task ValidateUserName(UserManager manager, TUser user, ICollection errors) { var userName = await manager.GetUserNameAsync(user); if (string.IsNullOrWhiteSpace(userName)) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PropertyTooShort, "UserName")); + errors.Add(Describer.InvalidUserName(userName)); } else if (manager.Options.User.UserNameValidationRegex != null && !Regex.IsMatch(userName, manager.Options.User.UserNameValidationRegex)) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.InvalidUserName, userName)); + errors.Add(Describer.InvalidUserName(userName)); } else { @@ -63,18 +69,18 @@ namespace Microsoft.AspNet.Identity if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateName, userName)); + errors.Add(Describer.DuplicateUserName(userName)); } } } // make sure email is not empty, valid, and unique - private static async Task ValidateEmail(UserManager manager, TUser user, List errors) + private async Task ValidateEmail(UserManager manager, TUser user, List errors) { var email = await manager.GetEmailAsync(user); if (string.IsNullOrWhiteSpace(email)) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.PropertyTooShort, "Email")); + errors.Add(Describer.InvalidEmail(email)); return; } #if ASPNET50 @@ -84,7 +90,7 @@ namespace Microsoft.AspNet.Identity } catch (FormatException) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.InvalidEmail, email)); + errors.Add(Describer.InvalidEmail(email)); return; } #endif @@ -92,7 +98,7 @@ namespace Microsoft.AspNet.Identity if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { - errors.Add(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateEmail, email)); + errors.Add(Describer.DuplicateEmail(email)); } } } diff --git a/src/Microsoft.AspNet.Identity/project.json b/src/Microsoft.AspNet.Identity/project.json index 0ef1603874..ad83945ac3 100644 --- a/src/Microsoft.AspNet.Identity/project.json +++ b/src/Microsoft.AspNet.Identity/project.json @@ -7,7 +7,8 @@ "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", "Microsoft.Framework.ConfigurationModel": "1.0.0-*", "Microsoft.Framework.DependencyInjection" : "1.0.0-*", - "Microsoft.Framework.OptionsModel": "1.0.0-*" + "Microsoft.Framework.OptionsModel": "1.0.0-*", + "Microsoft.Framework.Logging": "1.0.0-*" }, "frameworks": { "aspnet50": {}, diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs deleted file mode 100644 index 98c6e6506c..0000000000 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/EntityInMemoryTestServiceCollectionExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Identity.EntityFramework; -using Microsoft.AspNet.Identity.EntityFramework.InMemory.Test; -using Microsoft.Data.Entity; -using Microsoft.Framework.DependencyInjection; - -namespace Microsoft.AspNet.Identity -{ - public static class EntityInMemoryTestServiceCollectionExtensions - { - public static IdentityBuilder AddIdentityInMemory(this ServiceCollection services, InMemoryContext context) - { - return services.AddIdentityInMemory(context); - } - - public static IdentityBuilder AddIdentityInMemory(this ServiceCollection services, TDbContext context) - where TUser : IdentityUser - where TRole : IdentityRole - where TDbContext : DbContext - { - var builder = services.AddIdentity(); - builder.AddDefaultTokenProviders(); - services.AddInstance>(new InMemoryUserStore(context)); - var store = new RoleStore(context); - services.AddInstance>(store); - return builder; - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryContext.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryContext.cs index 375f24319d..16455fa25a 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryContext.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryContext.cs @@ -3,41 +3,25 @@ using System; using Microsoft.Data.Entity; -using Microsoft.Data.Entity.Metadata; namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test { public class InMemoryContext : - InMemoryContext + InMemoryContext { - public InMemoryContext() { } - public InMemoryContext(IServiceProvider serviceProvider) : base(serviceProvider) { } } public class InMemoryContext : - InMemoryContext + InMemoryContext where TUser : IdentityUser { - public InMemoryContext() { } - public InMemoryContext(IServiceProvider serviceProvider) : base(serviceProvider) { } } - public class InMemoryContext : DbContext + public class InMemoryContext : IdentityDbContext where TUser : IdentityUser where TRole : IdentityRole - where TUserLogin : IdentityUserLogin - where TUserRole : IdentityUserRole - where TUserClaim : IdentityUserClaim where TKey : IEquatable { - - public DbSet Users { get; set; } - public DbSet Roles { get; set; } - public DbSet RoleClaims { get; set; } - - public InMemoryContext(IServiceProvider serviceProvider) - : base(serviceProvider) { } - public InMemoryContext() { } protected override void OnConfiguring(DbContextOptions builder) @@ -45,50 +29,5 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test // Want fresh in memory store for tests always for now builder.UseInMemoryStore(persist: false); } - - protected override void OnModelCreating(ModelBuilder builder) - { - builder.Entity(b => - { - b.Key(u => u.Id); - b.Property(u => u.UserName); - b.ForRelational().Table("AspNetUsers"); - }); - - builder.Entity(b => - { - b.Key(r => r.Id); - b.ForRelational().Table("AspNetRoles"); - }); - - builder.Entity(b => - { - b.Key(r => new { r.UserId, r.RoleId }); - b.ForeignKey(f => f.UserId); - b.ForeignKey(f => f.RoleId); - b.ForRelational().Table("AspNetUserRoles"); - }); - - builder.Entity(b => - { - b.Key(l => new { l.LoginProvider, l.ProviderKey, l.UserId }); - b.ForeignKey(f => f.UserId); - b.ForRelational().Table("AspNetUserLogins"); - }); - - builder.Entity(b => - { - b.Key(c => c.Id); - b.ForeignKey(f => f.UserId); - b.ForRelational().Table("AspNetUserClaims"); - }); - - builder.Entity>(b => - { - b.Key(c => c.Id); - b.ForeignKey(f => f.RoleId); - b.ForRelational().Table("AspNetRoleClaims"); - }); - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryEFUserStoreTest.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryEFUserStoreTest.cs index e8bd3397b6..bbdf4a73db 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryEFUserStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryEFUserStoreTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNet.Identity.Test; using Microsoft.Framework.DependencyInjection; @@ -15,7 +16,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test protected override void AddUserStore(IServiceCollection services, object context = null) { - services.AddInstance>(new InMemoryUserStore((InMemoryContext)context)); + services.AddInstance>(new UserStore((InMemoryContext)context)); } protected override void AddRoleStore(IServiceCollection services, object context = null) diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs deleted file mode 100644 index e2e9f6e43d..0000000000 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/InMemoryUserStore.cs +++ /dev/null @@ -1,929 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. 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.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Entity; - -namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test -{ - public class InMemoryUserStore : InMemoryUserStore - { - public InMemoryUserStore(InMemoryContext context) : base(context) { } - } - - public class InMemoryUserStore : InMemoryUserStore - where TUser : IdentityUser - { - public InMemoryUserStore(InMemoryContext context) : base(context) { } - } - - public class InMemoryUserStore : InMemoryUserStore - where TUser:IdentityUser - where TContext : DbContext - { - public InMemoryUserStore(TContext context) : base(context) { } - } - - public class InMemoryUserStore : - IUserLoginStore, - IUserClaimStore, - IUserRoleStore, - IUserPasswordStore, - IUserSecurityStampStore, - IQueryableUserStore, - IUserEmailStore, - IUserPhoneNumberStore, - IUserTwoFactorStore, - IUserLockoutStore - where TKey : IEquatable - where TUser : IdentityUser - where TRole : IdentityRole - where TUserLogin : IdentityUserLogin, new() - where TUserRole : IdentityUserRole, new() - where TUserClaim : IdentityUserClaim, new() - where TContext : DbContext - { - private bool _disposed; - - public InMemoryUserStore(TContext context) - { - if (context == null) - { - throw new ArgumentNullException("context"); - } - Context = context; - AutoSaveChanges = true; - } - - public TContext Context { get; private set; } - - /// - /// If true will call SaveChanges after CreateAsync/UpdateAsync/DeleteAsync - /// - public bool AutoSaveChanges { get; set; } - - private Task SaveChanges(CancellationToken cancellationToken) - { - return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : Task.FromResult(0); - } - - protected virtual Task GetUserAggregate(Expression> filter, CancellationToken cancellationToken = default(CancellationToken)) - { - return Task.FromResult(Users.SingleOrDefault(filter)); - // TODO: return Users.SingleOrDefaultAsync(filter, cancellationToken); - //Include(u => u.Roles) - //.Include(u => u.Claims) - //.Include(u => u.Logins) - } - - public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(Convert.ToString(user.Id, CultureInfo.InvariantCulture)); - } - - public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.UserName); - } - - public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.UserName = userName; - return Task.FromResult(0); - } - - public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.NormalizedUserName); - } - - public Task SetNormalizedUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.NormalizedUserName = userName; - return Task.FromResult(0); - } - - public async virtual Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - await Context.AddAsync(user, cancellationToken); - await SaveChanges(cancellationToken); - } - - public async virtual Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - Context.Update(user); - await SaveChanges(cancellationToken); - } - - public async virtual Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - Context.Remove(user); - await SaveChanges(cancellationToken); - } - - public virtual TKey ConvertUserId(string userId) - { - return (TKey)Convert.ChangeType(userId, typeof(TKey)); - } - - /// - /// Find a user by id - /// - /// - /// - /// - public virtual Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var id = ConvertUserId(userId); - return GetUserAggregate(u => u.Id.Equals(id), cancellationToken); - } - - /// - /// Find a user by name - /// - /// - /// - /// - public virtual Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return GetUserAggregate(u => u.UserName.ToUpper() == userName.ToUpper(), cancellationToken); - } - - public IQueryable Users - { - get { return Context.Set(); } - } - - public async virtual Task AddLoginAsync(TUser user, UserLoginInfo login, - CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - var l = new TUserLogin - { - UserId = user.Id, - ProviderKey = login.ProviderKey, - LoginProvider = login.LoginProvider, - ProviderDisplayName = login.ProviderDisplayName - }; - await Context.Set().AddAsync(l, cancellationToken); - user.Logins.Add(l); - } - - public virtual Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, - CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - var entry = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - if (entry != null) - { - user.Logins.Remove(entry); - Context.Set>().Remove(entry); - } - return Task.FromResult(0); - } - - public virtual Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - IList result = user.Logins.Select( - l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)) - .ToList(); - return Task.FromResult(result); - } - - public async virtual Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - var userLogin = await Context.Set() - .FirstOrDefaultAsync(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); - if (userLogin != null) - { - return await GetUserAggregate(u => u.Id.Equals(userLogin.UserId), cancellationToken); - } - return null; - } - - /// - /// Set the password hash for a user - /// - /// - /// - /// - /// - public virtual Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.PasswordHash = passwordHash; - return Task.FromResult(0); - } - - /// - /// Get the password hash for a user - /// - /// - /// - /// - public virtual Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.PasswordHash); - } - - /// - /// Returns true if the user has a password set - /// - /// - /// - /// - public virtual Task HasPasswordAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(user.PasswordHash != null); - } - - /// - /// Return the claims for a user - /// - /// - /// - /// - public virtual Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - IList result = user.Claims.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList(); - return Task.FromResult(result); - } - - /// - /// Add claims to a user - /// - /// - /// - /// - /// - public virtual Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - if (claims == null) - { - throw new ArgumentNullException("claims"); - } - foreach (var claim in claims) - { - user.Claims.Add(new TUserClaim { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value }); - } - return Task.FromResult(0); - } - - /// - /// Updates the give claim with the new one. - /// - /// - /// - /// - /// - /// - public Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) - { - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - if (claim == null) - { - throw new ArgumentNullException("claim"); - } - if (newClaim == null) - { - throw new ArgumentNullException("newClaim"); - } - - var matchedClaim = user.Claims.FirstOrDefault(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type); - if(matchedClaim != null) - { - matchedClaim.ClaimValue = newClaim.Value; - matchedClaim.ClaimType = newClaim.Type; - } - return Task.FromResult(0); - } - - /// - /// Remove claims from a user - /// - /// - /// - /// - /// - public virtual Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - if (claims == null) - { - throw new ArgumentNullException("claims"); - } - foreach (var claim in claims) - { - var matchingClaims = - user.Claims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToList(); - foreach (var c in matchingClaims) - { - user.Claims.Remove(c); - } - } - // TODO:these claims might not exist in the dbset - //var query = - // _userClaims.Where( - // uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type); - //foreach (var c in query) - //{ - // _userClaims.Remove(c); - //} - return Task.FromResult(0); - } - - /// - /// Returns whether the user email is confirmed - /// - /// - /// - /// - public virtual Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.EmailConfirmed); - } - - /// - /// Set IsConfirmed on the user - /// - /// - /// - /// - /// - public virtual Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.EmailConfirmed = confirmed; - return Task.FromResult(0); - } - - /// - /// Set the user email - /// - /// - /// - /// - /// - public virtual Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.Email = email; - return Task.FromResult(0); - } - - /// - /// Get the user's email - /// - /// - /// - /// - public virtual Task GetEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.Email); - } - - /// - /// FindByLoginAsync a user by email - /// - /// - /// - /// - public virtual Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - return Task.FromResult(Users.SingleOrDefault(u => u.Email.ToUpper() == email.ToUpper())); - //return GetUserAggregate(u => u.Email.ToUpper() == email.ToUpper(), cancellationToken); - } - - /// - /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered - /// not locked out. - /// - /// - /// - /// - public virtual Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.LockoutEnd); - } - - /// - /// Locks a user out until the specified end date (set to a past date, to unlock a user) - /// - /// - /// - /// - /// - public virtual Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.LockoutEnd = lockoutEnd; - return Task.FromResult(0); - } - - /// - /// Used to record when an attempt to access the user has failed - /// - /// - /// - /// - public virtual Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.AccessFailedCount++; - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Used to reset the account access count, typically after the account is successfully accessed - /// - /// - /// - /// - public virtual Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.AccessFailedCount = 0; - return Task.FromResult(0); - } - - /// - /// 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. - /// - /// - /// - /// - public virtual Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.AccessFailedCount); - } - - /// - /// Returns whether the user can be locked out. - /// - /// - /// - /// - public virtual Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.LockoutEnabled); - } - - /// - /// Sets whether the user can be locked out. - /// - /// - /// - /// - /// - public virtual Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.LockoutEnabled = enabled; - return Task.FromResult(0); - } - - /// - /// Set the user's phone number - /// - /// - /// - /// - /// - public virtual Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.PhoneNumber = phoneNumber; - return Task.FromResult(0); - } - - /// - /// Get a user's phone number - /// - /// - /// - /// - public virtual Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.PhoneNumber); - } - - /// - /// Returns whether the user phoneNumber is confirmed - /// - /// - /// - /// - public virtual Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.PhoneNumberConfirmed); - } - - /// - /// Set PhoneNumberConfirmed on the user - /// - /// - /// - /// - /// - public virtual Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.PhoneNumberConfirmed = confirmed; - return Task.FromResult(0); - } - - /// - /// Add a user to a role - /// - /// - /// - /// - /// - public virtual Task AddToRoleAsync(TUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - // TODO: - //if (String.IsNullOrWhiteSpace(roleName)) - //{ - // throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "roleName"); - //} - var roleEntity = Context.Set().SingleOrDefault(r => r.Name.ToUpper() == roleName.ToUpper()); - if (roleEntity == null) - { - throw new InvalidOperationException("Role Not Found"); - //TODO: String.Format(CultureInfo.CurrentCulture, IdentityResources.RoleNotFound, roleName)); - } - var ur = new TUserRole { UserId = user.Id, RoleId = roleEntity.Id }; - user.Roles.Add(ur); - roleEntity.Users.Add(ur); - return Task.FromResult(0); - } - - /// - /// Remove a user from a role - /// - /// - /// - /// - /// - public virtual Task RemoveFromRoleAsync(TUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - //if (String.IsNullOrWhiteSpace(roleName)) - //{ - // throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "roleName"); - //} - var roleEntity = Context.Set().SingleOrDefault(r => r.Name.ToUpper() == roleName.ToUpper()); - if (roleEntity != null) - { - var userRole = user.Roles.FirstOrDefault(r => roleEntity.Id.Equals(r.RoleId)); - if (userRole != null) - { - user.Roles.Remove(userRole); - roleEntity.Users.Remove(userRole); - } - } - return Task.FromResult(0); - } - - /// - /// Get the names of the roles a user is a member of - /// - /// - /// - /// - public virtual Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - var query = from userRoles in user.Roles - join roles in Context.Set() - on userRoles.RoleId equals roles.Id - select roles.Name; - return Task.FromResult>(query.ToList()); - } - - /// - /// Returns true if the user is in the named role - /// - /// - /// - /// - /// - public virtual Task IsInRoleAsync(TUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - //if (String.IsNullOrWhiteSpace(roleName)) - //{ - // throw new ArgumentException(IdentityResources.ValueCannotBeNullOrEmpty, "roleName"); - //} - var any = - Context.Set().Where(r => r.Name.ToUpper() == roleName.ToUpper()) - .Where(r => r.Users.Any(ur => ur.UserId.Equals(user.Id))) - .Count() > 0; - return Task.FromResult(any); - } - - /// - /// Set the security stamp for the user - /// - /// - /// - /// - /// - public virtual Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.SecurityStamp = stamp; - return Task.FromResult(0); - } - - /// - /// Get the security stamp for a user - /// - /// - /// - /// - public virtual Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.SecurityStamp); - } - - /// - /// Set whether two factor authentication is enabled for the user - /// - /// - /// - /// - /// - public virtual Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - user.TwoFactorEnabled = enabled; - return Task.FromResult(0); - } - - /// - /// Gets whether two factor authentication is enabled for the user - /// - /// - /// - /// - public virtual Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - ThrowIfDisposed(); - if (user == null) - { - throw new ArgumentNullException("user"); - } - return Task.FromResult(user.TwoFactorEnabled); - } - - private void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - } - - /// - /// Dispose the store - /// - public void Dispose() - { - _disposed = true; - } - } -} diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs index 2efc785d71..9299a59ceb 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/RoleStoreTest.cs @@ -76,23 +76,10 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test var role = new IdentityRole("UpdateRoleName"); IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); Assert.Null(await manager.FindByNameAsync("New")); - role.Name = "New"; + IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, "New")); IdentityResultAssert.IsSuccess(await manager.UpdateAsync(role)); Assert.NotNull(await manager.FindByNameAsync("New")); Assert.Null(await manager.FindByNameAsync("UpdateAsync")); } - - [Fact] - public async Task CanSetUserName() - { - var manager = TestIdentityFactory.CreateRoleManager(); - var role = new IdentityRole("UpdateRoleName"); - IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); - Assert.Null(await manager.FindByNameAsync("New")); - IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, "New")); - Assert.NotNull(await manager.FindByNameAsync("New")); - Assert.Null(await manager.FindByNameAsync("UpdateAsync")); - } - } } diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs index 3112c72c58..5e4a8ef137 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/TestIdentityFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test services.AddEntityFramework().AddInMemoryStore(); var serviceProvider = services.BuildServiceProvider(); - var db = new InMemoryContext(serviceProvider); + var db = new InMemoryContext(); db.Database.EnsureCreated(); return db; diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/project.json b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/project.json index 38d3deabe7..e70b17a933 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/project.json +++ b/test/Microsoft.AspNet.Identity.EntityFramework.InMemory.Test/project.json @@ -4,14 +4,13 @@ "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Identity": "3.0.0-*", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.RequestContainer": "1.0.0-*", "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", "EntityFramework.InMemory": "7.0.0-*", "Microsoft.Framework.OptionsModel" : "1.0.0-*", "System.Security.Claims": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "code": "**\\*.cs;..\\Shared\\*.cs", "frameworks": { @@ -22,6 +21,6 @@ } }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" } } diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.Test/SqlStoreTestBase.cs b/test/Microsoft.AspNet.Identity.EntityFramework.Test/SqlStoreTestBase.cs index a5d0f07889..72b6275eda 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.Test/SqlStoreTestBase.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.Test/SqlStoreTestBase.cs @@ -7,10 +7,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Identity.Test; -using Microsoft.Data.Entity; using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.DependencyInjection.Fallback; -using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Runtime.Infrastructure; using Xunit; @@ -156,16 +153,90 @@ namespace Microsoft.AspNet.Identity.EntityFramework.Test IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); } - [Fact] - public async Task EnsureRoleClaimNavigationProperty() + private async Task LazyLoadTestSetup(TestDbContext db, TUser user) { var context = CreateContext(); - var roleManager = CreateRoleManager(context); - var r = CreateRole(); - IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(r)); - var c = new Claim("a", "b"); - IdentityResultAssert.IsSuccess(await roleManager.AddClaimAsync(r, c)); - Assert.NotNull(r.Claims.Single(cl => cl.ClaimValue == c.Value && cl.ClaimType == c.Type)); + var manager = CreateManager(context); + var role = CreateRoleManager(context); + var admin = CreateRole("Admin"); + var local = CreateRole("Local"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, new UserLoginInfo("provider", user.Id.ToString(), "display"))); + IdentityResultAssert.IsSuccess(await role.CreateAsync(admin)); + IdentityResultAssert.IsSuccess(await role.CreateAsync(local)); + IdentityResultAssert.IsSuccess(await manager.AddToRoleAsync(user, admin.Name)); + IdentityResultAssert.IsSuccess(await manager.AddToRoleAsync(user, local.Name)); + Claim[] userClaims = + { + new Claim("Whatever", "Value"), + new Claim("Whatever2", "Value2") + }; + foreach (var c in userClaims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + } + } + + [Fact] + public async Task LoadFromDbFindByIdTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + + var userById = await manager.FindByIdAsync(user.Id.ToString()); + Assert.Equal(2, (await manager.GetClaimsAsync(userById)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userById)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userById)).Count); + } + + [Fact] + public async Task LoadFromDbFindByNameTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + var userByName = await manager.FindByNameAsync(user.UserName); + Assert.Equal(2, (await manager.GetClaimsAsync(userByName)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByName)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByName)).Count); + } + + [Fact] + public async Task LoadFromDbFindByLoginTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + var userByLogin = await manager.FindByLoginAsync("provider", user.Id.ToString()); + Assert.Equal(2, (await manager.GetClaimsAsync(userByLogin)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByLogin)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByLogin)).Count); + } + + [Fact] + public async Task LoadFromDbFindByEmailTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + user.Email = "fooz@fizzy.pop"; + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + var userByEmail = await manager.FindByEmailAsync(user.Email); + Assert.Equal(2, (await manager.GetClaimsAsync(userByEmail)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByEmail)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByEmail)).Count); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs b/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs index 431d8cd1a3..c3a4ee18f0 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.EntityFramework.Test/UserStoreTest.cs @@ -265,18 +265,6 @@ namespace Microsoft.AspNet.Identity.EntityFramework.Test IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); } - [Fact] - public async Task EnsureRoleClaimNavigationProperty() - { - var context = CreateContext(); - var roleManager = CreateRoleManager(context); - var r = new IdentityRole("EnsureRoleClaimNavigationProperty"); - IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(r)); - var c = new Claim("a", "b"); - IdentityResultAssert.IsSuccess(await roleManager.AddClaimAsync(r, c)); - Assert.NotNull(r.Claims.Single(cl => cl.ClaimValue == c.Value && cl.ClaimType == c.Type)); - } - [Fact] public async Task AddUserToUnknownRoleFails() { @@ -287,6 +275,157 @@ namespace Microsoft.AspNet.Identity.EntityFramework.Test async () => await manager.AddToRoleAsync(u, "bogus")); } + [Fact] + public async Task ConcurrentUpdatesWillFail() + { + var user = CreateTestUser(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var user1 = await manager1.FindByIdAsync(user.Id); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user1); + Assert.NotNull(user2); + Assert.NotSame(user1, user2); + user1.UserName = Guid.NewGuid().ToString(); + user2.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), IdentityErrorDescriber.Default.ConcurrencyFailure()); + } + } + + [Fact] + public async Task ConcurrentUpdatesWillFailWithDetachedUser() + { + var user = CreateTestUser(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user2); + Assert.NotSame(user, user2); + user.UserName = Guid.NewGuid().ToString(); + user2.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), IdentityErrorDescriber.Default.ConcurrencyFailure()); + } + } + + [Fact] + public async Task DeleteAModifiedUserWillFail() + { + var user = CreateTestUser(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var user1 = await manager1.FindByIdAsync(user.Id); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user1); + Assert.NotNull(user2); + Assert.NotSame(user1, user2); + user1.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1)); + IdentityResultAssert.IsFailure(await manager2.DeleteAsync(user2), IdentityErrorDescriber.Default.ConcurrencyFailure()); + } + } + + [Fact] + public async Task ConcurrentRoleUpdatesWillFail() + { + var role = new IdentityRole(Guid.NewGuid().ToString()); + using (var db = CreateContext()) + { + var manager = CreateRoleManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateRoleManager(db); + var manager2 = CreateRoleManager(db2); + var role1 = await manager1.FindByIdAsync(role.Id); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role1); + Assert.NotNull(role2); + Assert.NotSame(role1, role2); + role1.Name = Guid.NewGuid().ToString(); + role2.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), IdentityErrorDescriber.Default.ConcurrencyFailure()); + } + } + + [Fact] + public async Task ConcurrentRoleUpdatesWillFailWithDetachedRole() + { + var role = new IdentityRole(Guid.NewGuid().ToString()); + using (var db = CreateContext()) + { + var manager = CreateRoleManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateRoleManager(db); + var manager2 = CreateRoleManager(db2); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role); + Assert.NotNull(role2); + Assert.NotSame(role, role2); + role.Name = Guid.NewGuid().ToString(); + role2.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), IdentityErrorDescriber.Default.ConcurrencyFailure()); + } + } + + [Fact] + public async Task DeleteAModifiedRoleWillFail() + { + var role = new IdentityRole(Guid.NewGuid().ToString()); + using (var db = CreateContext()) + { + var manager = CreateRoleManager(db); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateRoleManager(db); + var manager2 = CreateRoleManager(db2); + var role1 = await manager1.FindByIdAsync(role.Id); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role1); + Assert.NotNull(role2); + Assert.NotSame(role1, role2); + role1.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1)); + IdentityResultAssert.IsFailure(await manager2.DeleteAsync(role2), IdentityErrorDescriber.Default.ConcurrencyFailure()); + } + } + // TODO: can we move these to UserManagerTestBase? [Fact] public async Task DeleteRoleNonEmptySucceedsTest() diff --git a/test/Microsoft.AspNet.Identity.EntityFramework.Test/project.json b/test/Microsoft.AspNet.Identity.EntityFramework.Test/project.json index b4f14912d0..3dc499053e 100644 --- a/test/Microsoft.AspNet.Identity.EntityFramework.Test/project.json +++ b/test/Microsoft.AspNet.Identity.EntityFramework.Test/project.json @@ -4,7 +4,6 @@ "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Identity": "3.0.0-*", "Microsoft.AspNet.Identity.EntityFramework": "1.0.0-*", - "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.RequestContainer": "1.0.0-*", "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", @@ -12,7 +11,7 @@ "EntityFramework.SqlServer": "7.0.0-*", "Microsoft.Framework.OptionsModel" : "1.0.0-*", "System.Security.Claims": "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "code": "**\\*.cs;..\\Shared\\*.cs", "frameworks": { @@ -23,6 +22,6 @@ } }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" } } diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs index ade7e69e73..9f922d9a3b 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/HttpSignInTest.cs @@ -4,6 +4,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Identity.Test; @@ -14,7 +15,7 @@ using Xunit; namespace Microsoft.AspNet.Identity.InMemory.Test { - public class ApplicationUser : IdentityUser { } + public class ApplicationUser : InMemoryUser { } public class HttpSignInTest { @@ -30,7 +31,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.IsAny())).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); app.UseServices(services => { @@ -50,10 +51,10 @@ namespace Microsoft.AspNet.Identity.InMemory.Test var signInManager = app.ApplicationServices.GetRequiredService>(); IdentityResultAssert.IsSuccess(await userManager.CreateAsync(user, password)); - var result = await signInManager.PasswordSignInAsync(user.UserName, password, isPersistent, false); + var result = await signInManager.PasswordSignInAsync(user, password, isPersistent, false); // Assert - Assert.Equal(SignInStatus.Success, result); + Assert.True(result.Succeeded); context.VerifyAll(); response.VerifyAll(); contextAccessor.VerifyAll(); diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs index 0404ef3939..0f250c4615 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryRoleStore.cs @@ -15,20 +15,20 @@ namespace Microsoft.AspNet.Identity.InMemory { private readonly Dictionary _roles = new Dictionary(); - public Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { _roles[role.Id] = role; - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } - public Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { if (role == null || !_roles.ContainsKey(role.Id)) { throw new InvalidOperationException("Unknown role"); } _roles.Remove(role.Id); - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) @@ -47,10 +47,10 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } - public Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) { _roles[role.Id] = role; - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default(CancellationToken)) @@ -97,6 +97,17 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } + public Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(role.NormalizedName); + } + + public Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + role.NormalizedName = normalizedName; + return Task.FromResult(0); + } + public IQueryable Roles { get { return _roles.Values.AsQueryable(); } diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs index e3de60ca21..4e034f666e 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNet.Identity.Test; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Identity.InMemory.Test { - public class InMemoryStoreTest : UserManagerTestBase + public class InMemoryStoreTest : UserManagerTestBase { protected override object CreateTestContext() { @@ -15,7 +16,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test protected override void AddUserStore(IServiceCollection services, object context = null) { - services.AddSingleton, InMemoryUserStore>(); + services.AddSingleton, InMemoryUserStore>(); } protected override void AddRoleStore(IServiceCollection services, object context = null) diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs index e056b3f048..ab3521f6b4 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryUserStore.cs @@ -10,6 +10,30 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Identity.InMemory { + public class InMemoryUser : IdentityUser + { + public InMemoryUser() { } + + public InMemoryUser(string userName) : base(userName) { } + + + /// + /// Roles for the user + /// + public virtual ICollection Roles { get; } = new List(); + + /// + /// Claims for the user + /// + public virtual ICollection Claims { get; } = new List(); + + /// + /// Associated logins for the user + /// + public virtual ICollection Logins { get; } = new List(); + + } + public class InMemoryUserStore : IUserLoginStore, IUserRoleStore, @@ -21,7 +45,7 @@ namespace Microsoft.AspNet.Identity.InMemory IUserPhoneNumberStore, IQueryableUserStore, IUserTwoFactorStore - where TUser : IdentityUser + where TUser : InMemoryUser { private readonly Dictionary _logins = new Dictionary(); @@ -42,7 +66,7 @@ namespace Microsoft.AspNet.Identity.InMemory { foreach (var claim in claims) { - user.Claims.Add(new IdentityUserClaim { ClaimType = claim.Type, ClaimValue = claim.Value, UserId = user.Id }); + user.Claims.Add(new IdentityUserClaim { ClaimType = claim.Type, ClaimValue = claim.Value, UserId = user.Id }); } return Task.FromResult(0); } @@ -50,7 +74,7 @@ namespace Microsoft.AspNet.Identity.InMemory public Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) { var matchedClaims = user.Claims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToList(); - foreach(var matchedClaim in matchedClaims) + foreach (var matchedClaim in matchedClaims) { matchedClaim.ClaimValue = newClaim.Value; matchedClaim.ClaimType = newClaim.Type; @@ -84,6 +108,18 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(user.Email); } + public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(user.NormalizedEmail); + } + + public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + user.NormalizedEmail = normalizedEmail; + return Task.FromResult(0); + } + + public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(user.EmailConfirmed); @@ -99,7 +135,7 @@ namespace Microsoft.AspNet.Identity.InMemory { return Task.FromResult( - Users.FirstOrDefault(u => String.Equals(u.Email, email, StringComparison.OrdinalIgnoreCase))); + Users.FirstOrDefault(u => u.NormalizedEmail == email)); } public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) @@ -149,7 +185,7 @@ namespace Microsoft.AspNet.Identity.InMemory public virtual Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) { - user.Logins.Add(new IdentityUserLogin + user.Logins.Add(new IdentityUserLogin { UserId = user.Id, ProviderKey = login.ProviderKey, @@ -209,16 +245,16 @@ namespace Microsoft.AspNet.Identity.InMemory return Task.FromResult(0); } - public Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { _users[user.Id] = user; - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } - public Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { _users[user.Id] = user; - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) @@ -238,17 +274,17 @@ namespace Microsoft.AspNet.Identity.InMemory { return Task.FromResult( - Users.FirstOrDefault(u => String.Equals(u.UserName, userName, StringComparison.OrdinalIgnoreCase))); + Users.FirstOrDefault(u => u.NormalizedUserName == userName)); } - public Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) { if (user == null || !_users.ContainsKey(user.Id)) { throw new InvalidOperationException("Unknown user"); } _users.Remove(user.Id); - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) @@ -292,7 +328,7 @@ namespace Microsoft.AspNet.Identity.InMemory // RoleId == roleName for InMemory public Task AddToRoleAsync(TUser user, string role, CancellationToken cancellationToken = default(CancellationToken)) { - user.Roles.Add(new IdentityUserRole { RoleId = role, UserId = user.Id }); + user.Roles.Add(new IdentityUserRole { RoleId = role, UserId = user.Id }); return Task.FromResult(0); } @@ -349,5 +385,30 @@ namespace Microsoft.AspNet.Identity.InMemory user.NormalizedUserName = userName; return Task.FromResult(0); } + + // RoleId == rolename for inmemory store tests + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + if (String.IsNullOrEmpty(roleName)) + { + throw new ArgumentNullException("role"); + } + + return Task.FromResult>(Users.Where(u => (u.Roles.Where(x => x.RoleId == roleName).Count() > 0)).Select(x => x).ToList()); + } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + if (claim == null) + { + throw new ArgumentNullException("claim"); + } + + var query = from user in Users + where user.Claims.Where(x => x.ClaimType == claim.Type && x.ClaimValue == claim.Value).FirstOrDefault() != null + select user; + + return Task.FromResult>(query.ToList()); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/project.json b/test/Microsoft.AspNet.Identity.InMemory.Test/project.json index 470267ba51..c9eef4b87a 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/project.json +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/project.json @@ -3,7 +3,6 @@ "Microsoft.AspNet.Hosting": "1.0.0-*", "Microsoft.AspNet.Http" : "1.0.0-*", "Microsoft.AspNet.Identity" : "3.0.0-*", - "Microsoft.AspNet.PipelineCore" : "1.0.0-*", "Microsoft.AspNet.RequestContainer" : "1.0.0-*", "Microsoft.AspNet.Security" : "1.0.0-*", "Microsoft.AspNet.Security.Cookies" : "1.0.0-*", @@ -11,7 +10,7 @@ "Microsoft.Framework.ConfigurationModel": "1.0.0-*", "Microsoft.Framework.DependencyInjection" : "1.0.0-*", "Microsoft.Framework.OptionsModel" : "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "code": "**\\*.cs;..\\Shared\\*.cs", "frameworks": { @@ -22,6 +21,6 @@ } }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" } } diff --git a/test/Microsoft.AspNet.Identity.Test/IHttpContextAccessor.cs b/test/Microsoft.AspNet.Identity.Test/IHttpContextAccessor.cs new file mode 100644 index 0000000000..b812ed6e8b --- /dev/null +++ b/test/Microsoft.AspNet.Identity.Test/IHttpContextAccessor.cs @@ -0,0 +1,6 @@ +namespace Microsoft.AspNet.Identity.Test +{ + internal interface IHttpContextAccessor + { + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs index 0bbf0f7f11..ee4b210d7b 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityBuilderTest.cs @@ -10,7 +10,7 @@ using System; using System.Threading; using System.Threading.Tasks; using System.Linq; -using Microsoft.AspNet.Security.DataProtection; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Identity.Test { @@ -62,6 +62,28 @@ namespace Microsoft.AspNet.Identity.Test Assert.NotNull(thingy); } + [Fact] + public void CanOverrideUserManager() + { + var services = new ServiceCollection(); + services.AddIdentity() + .AddUserStore() + .AddUserManager(); + var myUserManager = services.BuildServiceProvider().GetRequiredService(typeof(UserManager)) as MyUserManager; + Assert.NotNull(myUserManager); + } + + [Fact] + public void CanOverrideRoleManager() + { + var services = new ServiceCollection(); + services.AddIdentity() + .AddRoleStore() + .AddRoleManager(); + var myRoleManager = services.BuildServiceProvider().GetRequiredService>() as MyRoleManager; + Assert.NotNull(myRoleManager); + } + [Fact] public void EnsureDefaultServices() { @@ -92,22 +114,22 @@ namespace Microsoft.AspNet.Identity.Test private class MyUberThingy : IUserValidator, IPasswordValidator, IRoleValidator, IUserStore, IRoleStore { - public Task CreateAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } - public Task CreateAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } - public Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } - public Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } @@ -127,6 +149,11 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } + public Task GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); @@ -152,6 +179,11 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } + public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); @@ -167,12 +199,12 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } - public Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } - public Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } @@ -203,5 +235,18 @@ namespace Microsoft.AspNet.Identity.Test } } + private class MyUserManager : UserManager + { + public MyUserManager(IUserStore store) : base(store) { } + } + + private class MyRoleManager : RoleManager + { + public MyRoleManager(IRoleStore store, + IEnumerable> roleValidators) : base(store) + { + + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs index 908c73ff82..823d0c639c 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityOptionsTest.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Equal(ClaimTypes.Role, options.ClaimsIdentity.RoleClaimType); Assert.Equal(ClaimTypes.Name, options.ClaimsIdentity.UserNameClaimType); Assert.Equal(ClaimTypes.NameIdentifier, options.ClaimsIdentity.UserIdClaimType); - Assert.Equal(ClaimsIdentityOptions.DefaultSecurityStampClaimType, options.ClaimsIdentity.SecurityStampClaimType); + Assert.Equal("AspNet.Identity.SecurityStamp", options.ClaimsIdentity.SecurityStampClaimType); } [Theory] diff --git a/test/Microsoft.AspNet.Identity.Test/IdentityResultTest.cs b/test/Microsoft.AspNet.Identity.Test/IdentityResultTest.cs index 0fd8e3bddd..f65ab23cfb 100644 --- a/test/Microsoft.AspNet.Identity.Test/IdentityResultTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/IdentityResultTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using System.Text; using Xunit; namespace Microsoft.AspNet.Identity.Test @@ -13,17 +14,39 @@ namespace Microsoft.AspNet.Identity.Test { var result = new IdentityResult(); Assert.False(result.Succeeded); - Assert.Equal(1, result.Errors.Count()); - Assert.Equal("An unknown failure has occured.", result.Errors.First()); + Assert.Equal(0, result.Errors.Count()); } [Fact] - public void NullErrorListUsesDefaultError() + public void NullFailedUsesEmptyErrors() { - var result = new IdentityResult(null); + var result = IdentityResult.Failed(); Assert.False(result.Succeeded); - Assert.Equal(1, result.Errors.Count()); - Assert.Equal("An unknown failure has occured.", result.Errors.First()); + Assert.Equal(0, result.Errors.Count()); + } + + [Fact] + public void VerifySuccessResultLog() + { + var result = IdentityResult.Success; + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Success", logMessage.ToString()); + } + + [Fact] + public void VerifyFailureResultLog() + { + var result = IdentityResult.Failed(new IdentityError() { Code = "Foo" }, new IdentityError() { Code = "Bar" }); + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Failed : Foo,Bar", logMessage.ToString()); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/Microsoft.AspNet.Identity.Test.kproj b/test/Microsoft.AspNet.Identity.Test/Microsoft.AspNet.Identity.Test.kproj index 1bbfe3ac3e..3c06d031ab 100644 --- a/test/Microsoft.AspNet.Identity.Test/Microsoft.AspNet.Identity.Test.kproj +++ b/test/Microsoft.AspNet.Identity.Test/Microsoft.AspNet.Identity.Test.kproj @@ -1,4 +1,4 @@ - + 14.0 @@ -14,4 +14,9 @@ 2.0 - + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs b/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs index 004d960841..4d62b972bf 100644 --- a/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs +++ b/test/Microsoft.AspNet.Identity.Test/NoopRoleStore.cs @@ -8,14 +8,14 @@ namespace Microsoft.AspNet.Identity.Test { public class NoopRoleStore : IRoleStore { - public Task CreateAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } - public Task UpdateAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task GetRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) @@ -42,14 +42,24 @@ namespace Microsoft.AspNet.Identity.Test { } - public Task DeleteAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task GetRoleIdAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(null); } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/NoopUserStore.cs b/test/Microsoft.AspNet.Identity.Test/NoopUserStore.cs index 026784e489..86078e2c0e 100644 --- a/test/Microsoft.AspNet.Identity.Test/NoopUserStore.cs +++ b/test/Microsoft.AspNet.Identity.Test/NoopUserStore.cs @@ -24,14 +24,14 @@ namespace Microsoft.AspNet.Identity.Test return Task.FromResult(0); } - public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } - public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) @@ -48,9 +48,9 @@ namespace Microsoft.AspNet.Identity.Test { } - public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs index 220fb2125b..f8c37de273 100644 --- a/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/PasswordValidatorTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -133,13 +134,18 @@ namespace Microsoft.AspNet.Identity.Test { errors.Add(upperError); } + var result = await valid.ValidateAsync(manager, null, input); if (errors.Count == 0) { - IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input)); + IdentityResultAssert.IsSuccess(result); } else { - IdentityResultAssert.IsFailure(await valid.ValidateAsync(manager, null, input), string.Join(" ", errors)); + IdentityResultAssert.IsFailure(result); + foreach (var error in errors) + { + Assert.True(result.Errors.Any(e => e.Description == error)); + } } } } diff --git a/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs index 31fa379c79..99e35a31fb 100644 --- a/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/RoleManagerTest.cs @@ -6,12 +6,51 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Moq; using Xunit; namespace Microsoft.AspNet.Identity.Test { public class RoleManagerTest { + [Fact] + public async Task CreateCallsStore() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.CreateAsync(role, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetRoleNameAsync(role, CancellationToken.None)).Returns(Task.FromResult(role.Name)).Verifiable(); + store.Setup(s => s.SetNormalizedRoleNameAsync(role, role.Name.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var roleManager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await roleManager.CreateAsync(role); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateCallsStore() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.UpdateAsync(role, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetRoleNameAsync(role, CancellationToken.None)).Returns(Task.FromResult(role.Name)).Verifiable(); + store.Setup(s => s.SetNormalizedRoleNameAsync(role, role.Name.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var roleManager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await roleManager.UpdateAsync(role); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + [Fact] public void RolesQueryableFailWhenStoreNotImplemented() { @@ -20,6 +59,59 @@ namespace Microsoft.AspNet.Identity.Test Assert.Throws(() => manager.Roles.Count()); } + [Fact] + public async Task FindByNameCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync("FOO", CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await manager.FindByNameAsync(role.Name); + + // Assert + Assert.Equal(role, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByNameCallsStoreWithoutNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync(role.Name, CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + manager.KeyNormalizer = null; + + // Act + var result = await manager.FindByNameAsync(role.Name); + + // Assert + Assert.Equal(role, result); + store.VerifyAll(); + } + + [Fact] + public async Task RoleExistsCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync("FOO", CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await manager.RoleExistsAsync(role.Name); + + // Assert + Assert.True(result); + store.VerifyAll(); + } + + [Fact] public void DisposeAfterDisposeDoesNotThrow() { @@ -32,7 +124,7 @@ namespace Microsoft.AspNet.Identity.Test public async Task RoleManagerPublicNullChecks() { Assert.Throws("store", - () => new RoleManager(null, null)); + () => new RoleManager(null, null, null)); var manager = CreateRoleManager(new NotImplementedStore()); await Assert.ThrowsAsync("role", async () => await manager.CreateAsync(null)); await Assert.ThrowsAsync("role", async () => await manager.UpdateAsync(null)); @@ -58,22 +150,22 @@ namespace Microsoft.AspNet.Identity.Test { var v = new List>(); v.Add(new RoleValidator()); - return new RoleManager(roleStore, v); + return new RoleManager(roleStore); } private class NotImplementedStore : IRoleStore { - public Task CreateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } - public Task UpdateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } - public Task DeleteAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); } @@ -107,6 +199,16 @@ namespace Microsoft.AspNet.Identity.Test { throw new NotImplementedException(); } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/RoleValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/RoleValidatorTest.cs index c2f47f6870..f1b0aa48fa 100644 --- a/test/Microsoft.AspNet.Identity.Test/RoleValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/RoleValidatorTest.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Identity.Test { // Setup var validator = new RoleValidator(); - var manager = new RoleManager(new NoopRoleStore(), null); + var manager = new RoleManager(new NoopRoleStore()); // Act // Assert @@ -29,14 +29,14 @@ namespace Microsoft.AspNet.Identity.Test { // Setup var validator = new RoleValidator(); - var manager = new RoleManager(new NoopRoleStore(), null); + var manager = new RoleManager(new NoopRoleStore()); var user = new TestRole {Name = input}; // Act var result = await validator.ValidateAsync(manager, user); // Assert - IdentityResultAssert.IsFailure(result, "Name cannot be null or empty."); + IdentityResultAssert.IsFailure(result, IdentityErrorDescriber.Default.InvalidRoleName(input)); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs index 5ae623e01d..4bad958493 100644 --- a/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/SecurityStampValidatorTest.cs @@ -5,6 +5,7 @@ using System; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security; @@ -43,10 +44,10 @@ namespace Microsoft.AspNet.Identity.Test var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var httpContext = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - contextAccessor.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object, null); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).ReturnsAsync(user).Verifiable(); signInManager.Setup(s => s.SignInAsync(user, isPersistent, null, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); var services = new ServiceCollection(); @@ -78,10 +79,10 @@ namespace Microsoft.AspNet.Identity.Test var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var httpContext = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - contextAccessor.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object, null); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).ReturnsAsync(null).Verifiable(); var services = new ServiceCollection(); services.AddInstance(options.Object); @@ -112,10 +113,10 @@ namespace Microsoft.AspNet.Identity.Test var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.Zero }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - contextAccessor.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object, null); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).ReturnsAsync(null).Verifiable(); var services = new ServiceCollection(); services.AddInstance(options.Object); @@ -146,10 +147,10 @@ namespace Microsoft.AspNet.Identity.Test var identityOptions = new IdentityOptions { SecurityStampValidationInterval = TimeSpan.FromDays(1) }; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(httpContext.Object); var signInManager = new Mock>(userManager.Object, - contextAccessor.Object, claimsManager.Object, options.Object); + contextAccessor.Object, claimsManager.Object, options.Object, null); signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny(), user.Id, CancellationToken.None)).Throws(new Exception("Shouldn't be called")); signInManager.Setup(s => s.SignInAsync(user, false, null, CancellationToken.None)).Throws(new Exception("Shouldn't be called")); var services = new ServiceCollection(); diff --git a/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs index 629311bf23..401d4cf384 100644 --- a/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/SignInManagerTest.cs @@ -1,17 +1,19 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Security.Claims; +using System.Security.Principal; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Security; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; using Moq; -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; using Xunit; namespace Microsoft.AspNet.Identity.Test @@ -31,7 +33,7 @@ namespace Microsoft.AspNet.Identity.Test // TODO: how to functionally test context? // var context = new DefaultHttpContext(new FeatureCollection()); - // var contextAccessor = new Mock>(); + // var contextAccessor = new Mock(); // contextAccessor.Setup(a => a.Value).Returns(context); // app.UseServices(services => // { @@ -67,16 +69,14 @@ namespace Microsoft.AspNet.Identity.Test [Fact] public void ConstructorNullChecks() { - Assert.Throws("userManager", () => new SignInManager(null, null, null, null)); + Assert.Throws("userManager", () => new SignInManager(null, null, null, null, null)); var userManager = MockHelpers.MockUserManager().Object; Assert.Throws("contextAccessor", () => new SignInManager(userManager, null, null, null)); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); Assert.Throws("contextAccessor", () => new SignInManager(userManager, contextAccessor.Object, null, null)); var context = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); Assert.Throws("claimsFactory", () => new SignInManager(userManager, contextAccessor.Object, null, null)); - var claimsFactory = new Mock>().Object; - Assert.Throws("optionsAccessor", () => new SignInManager(userManager, contextAccessor.Object, claimsFactory, null)); } //TODO: Mock fails in K (this works fine in net45) @@ -95,7 +95,7 @@ namespace Microsoft.AspNet.Identity.Test // var response = new Mock(); // context.Setup(c => c.Response).Returns(response.Object).Verifiable(); // response.Setup(r => r.SignIn(testIdentity, It.IsAny())).Verifiable(); - // var contextAccessor = new Mock>(); + // var contextAccessor = new Mock(); // contextAccessor.Setup(a => a.Value).Returns(context.Object); // var helper = new HttpAuthenticationManager(contextAccessor.Object); @@ -118,24 +118,31 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); manager.Setup(m => m.IsLockedOutAsync(user, CancellationToken.None)).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); + var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "PasswordSignInAsync", user.Id, "Lockedout"); // Act var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false); // Assert - Assert.Equal(SignInStatus.LockedOut, result); + Assert.False(result.Succeeded); + Assert.True(result.IsLockedOut); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); } - + [Theory] [InlineData(true)] [InlineData(false)] @@ -149,11 +156,13 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "password", CancellationToken.None)).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.ResetAccessFailedCountAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); + var context = new Mock(); var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == isPersistent), It.IsAny())).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -161,13 +170,17 @@ namespace Microsoft.AspNet.Identity.Test options.Setup(a => a.Options).Returns(identityOptions); var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); claimsFactory.Setup(m => m.CreateAsync(user, CancellationToken.None)).ReturnsAsync(new ClaimsIdentity("Microsoft.AspNet.Identity")).Verifiable(); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "PasswordSignInAsync", user.Id, "Succeeded"); // Act var result = await helper.PasswordSignInAsync(user.UserName, "password", isPersistent, false); // Assert - Assert.Equal(SignInStatus.Success, result); + Assert.True(result.Succeeded); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -186,11 +199,13 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "password", CancellationToken.None)).ReturnsAsync(true).Verifiable(); manager.Setup(m => m.ResetAccessFailedCountAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); + var context = new Mock(); var response = new Mock(); response.Setup(r => r.SignIn(It.IsAny(), It.IsAny())).Verifiable(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -203,7 +218,7 @@ namespace Microsoft.AspNet.Identity.Test var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); // Assert - Assert.Equal(SignInStatus.Success, result); + Assert.True(result.Succeeded); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -239,19 +254,24 @@ namespace Microsoft.AspNet.Identity.Test var response = new Mock(); response.Setup(r => r.SignIn(It.Is(id => id.Name == user.Id))).Verifiable(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); - var helper = new SignInManager(manager.Object, contextAccessor.Object, new ClaimsIdentityFactory(manager.Object, roleManager.Object, options.Object), options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, new ClaimsIdentityFactory(manager.Object, roleManager.Object, options.Object), options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "PasswordSignInAsync", user.Id, "RequiresTwoFactor"); // Act var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); // Assert - Assert.Equal(SignInStatus.RequiresVerification, result); + Assert.False(result.Succeeded); + Assert.True(result.RequiresTwoFactor); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -276,13 +296,15 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.IsLockedOutAsync(user, CancellationToken.None)).ReturnsAsync(false).Verifiable(); } manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey, CancellationToken.None)).ReturnsAsync(user).Verifiable(); + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); + var context = new Mock(); var response = new Mock(); context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn( It.Is(v => v.IsPersistent == isPersistent), It.Is(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider))).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -291,13 +313,17 @@ namespace Microsoft.AspNet.Identity.Test options.Setup(a => a.Options).Returns(identityOptions); var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); claimsFactory.Setup(m => m.CreateAsync(user, CancellationToken.None)).ReturnsAsync(new ClaimsIdentity("Microsoft.AspNet.Identity")).Verifiable(); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "ExternalLoginSignInAsync", user.Id.ToString(), "Succeeded"); // Act var result = await helper.ExternalLoginSignInAsync(loginProvider, providerKey, isPersistent); // Assert - Assert.Equal(SignInStatus.Success, result); + Assert.True(result.Succeeded); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -322,7 +348,6 @@ namespace Microsoft.AspNet.Identity.Test [InlineData(false, true, false, false)] [InlineData(false, false, true, false)] [InlineData(false, false, false, false)] - public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient) { // Setup @@ -342,7 +367,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.GetUserNameAsync(user, CancellationToken.None)).ReturnsAsync(user.UserName).Verifiable(); var context = new Mock(); var response = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); var twoFactorInfo = new SignInManager.TwoFactorAuthenticationInfo { UserId = user.Id }; var loginProvider = "loginprovider"; var id = SignInManager.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null); @@ -376,13 +401,17 @@ namespace Microsoft.AspNet.Identity.Test context.Setup(c => c.Response).Returns(response.Object).Verifiable(); context.Setup(c => c.AuthenticateAsync(IdentityOptions.TwoFactorUserIdCookieAuthenticationType)).ReturnsAsync(authResult).Verifiable(); contextAccessor.Setup(a => a.Value).Returns(context.Object); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "TwoFactorSignInAsync", user.Id.ToString(), "Succeeded"); // Act var result = await helper.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient); // Assert - Assert.Equal(SignInStatus.Success, result); + Assert.True(result.Succeeded); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -397,7 +426,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = MockHelpers.MockUserManager(); var context = new Mock(); var response = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); var options = new Mock>(); @@ -452,7 +481,7 @@ namespace Microsoft.AspNet.Identity.Test id.AddClaim(new Claim(ClaimTypes.Name, user.Id)); var authResult = new AuthenticationResult(id, new AuthenticationProperties(), new AuthenticationDescription()); context.Setup(c => c.AuthenticateAsync(IdentityOptions.TwoFactorRememberMeCookieAuthenticationType)).ReturnsAsync(authResult).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -466,7 +495,7 @@ namespace Microsoft.AspNet.Identity.Test var result = await helper.PasswordSignInAsync(user.UserName, "password", isPersistent, false); // Assert - Assert.Equal(SignInStatus.Success, result); + Assert.True(result.Succeeded); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -487,7 +516,7 @@ namespace Microsoft.AspNet.Identity.Test response.Setup(r => r.SignOut(authenticationType)).Verifiable(); response.Setup(r => r.SignOut(IdentityOptions.TwoFactorUserIdCookieAuthenticationType)).Verifiable(); response.Setup(r => r.SignOut(IdentityOptions.ExternalCookieAuthenticationType)).Verifiable(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -495,7 +524,9 @@ namespace Microsoft.AspNet.Identity.Test options.Setup(a => a.Options).Returns(identityOptions); IdentityOptions.ApplicationCookieAuthenticationType = authenticationType; var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); // Act helper.SignOut(); @@ -517,20 +548,25 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.IsLockedOutAsync(user, CancellationToken.None)).ReturnsAsync(false).Verifiable(); manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "bogus", CancellationToken.None)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "PasswordSignInAsync", user.Id.ToString(), "Failed"); // Act var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false); // Assert - Assert.Equal(SignInStatus.Failure, result); + Assert.False(result.Succeeded); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); contextAccessor.VerifyAll(); @@ -543,7 +579,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = MockHelpers.MockUserManager(); manager.Setup(m => m.FindByNameAsync("bogus", CancellationToken.None)).ReturnsAsync(null).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -556,7 +592,7 @@ namespace Microsoft.AspNet.Identity.Test var result = await helper.PasswordSignInAsync("bogus", "bogus", false, false); // Assert - Assert.Equal(SignInStatus.Failure, result); + Assert.False(result.Succeeded); manager.VerifyAll(); context.VerifyAll(); contextAccessor.VerifyAll(); @@ -579,7 +615,7 @@ namespace Microsoft.AspNet.Identity.Test manager.Setup(m => m.FindByNameAsync(user.UserName, CancellationToken.None)).ReturnsAsync(user).Verifiable(); manager.Setup(m => m.CheckPasswordAsync(user, "bogus", CancellationToken.None)).ReturnsAsync(false).Verifiable(); var context = new Mock(); - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -592,7 +628,8 @@ namespace Microsoft.AspNet.Identity.Test var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, true); // Assert - Assert.Equal(SignInStatus.LockedOut, result); + Assert.False(result.Succeeded); + Assert.True(result.IsLockedOut); manager.VerifyAll(); } @@ -609,6 +646,7 @@ namespace Microsoft.AspNet.Identity.Test { manager.Setup(m => m.CheckPasswordAsync(user, "password", CancellationToken.None)).ReturnsAsync(true).Verifiable(); } + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); var context = new Mock(); var response = new Mock(); if (confirmed) @@ -617,7 +655,7 @@ namespace Microsoft.AspNet.Identity.Test context.Setup(c => c.Response).Returns(response.Object).Verifiable(); response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == false), It.IsAny())).Verifiable(); } - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); @@ -625,13 +663,19 @@ namespace Microsoft.AspNet.Identity.Test var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "CanSignInAsync", user.Id.ToString(), confirmed.ToString()); // Act var result = await helper.PasswordSignInAsync(user, "password", false, false); // Assert - Assert.Equal(confirmed ? SignInStatus.Success : SignInStatus.NotAllowed, result); + + Assert.Equal(confirmed, result.Succeeded); + Assert.NotEqual(confirmed, result.IsNotAllowed); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); @@ -646,7 +690,8 @@ namespace Microsoft.AspNet.Identity.Test // Setup var user = new TestUser { UserName = "Foo" }; var manager = MockHelpers.MockUserManager(); - manager.Setup(m => m.IsEmailConfirmedAsync(user, CancellationToken.None)).ReturnsAsync(confirmed).Verifiable(); + manager.Setup(m => m.IsPhoneNumberConfirmedAsync(user, CancellationToken.None)).ReturnsAsync(confirmed).Verifiable(); + manager.Setup(m => m.GetUserIdAsync(user, CancellationToken.None)).ReturnsAsync(user.Id.ToString()).Verifiable(); var context = new Mock(); var response = new Mock(); if (confirmed) @@ -656,21 +701,26 @@ namespace Microsoft.AspNet.Identity.Test response.Setup(r => r.SignIn(It.Is(v => v.IsPersistent == false), It.IsAny())).Verifiable(); } - var contextAccessor = new Mock>(); + var contextAccessor = new Mock(); contextAccessor.Setup(a => a.Value).Returns(context.Object); var roleManager = MockHelpers.MockRoleManager(); var identityOptions = new IdentityOptions(); - identityOptions.SignIn.RequireConfirmedEmail = true; + identityOptions.SignIn.RequireConfirmedPhoneNumber = true; var options = new Mock>(); options.Setup(a => a.Options).Returns(identityOptions); var claimsFactory = new Mock>(manager.Object, roleManager.Object, options.Object); - var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object); + var logStore = new StringBuilder(); + var loggerFactory = MockHelpers.MockILoggerFactory(MockHelpers.MockILogger(logStore).Object); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory.Object, options.Object, loggerFactory.Object); + string expected = string.Format("{0} for user: {1} : Result : {2}", "CanSignInAsync", user.Id.ToString(), confirmed.ToString()); // Act var result = await helper.PasswordSignInAsync(user, "password", false, false); // Assert - Assert.Equal(confirmed ? SignInStatus.Success : SignInStatus.NotAllowed, result); + Assert.Equal(confirmed, result.Succeeded); + Assert.NotEqual(confirmed, result.IsNotAllowed); + Assert.NotEqual(-1, logStore.ToString().IndexOf(expected)); manager.VerifyAll(); context.VerifyAll(); response.VerifyAll(); diff --git a/test/Microsoft.AspNet.Identity.Test/SignInResultTest.cs b/test/Microsoft.AspNet.Identity.Test/SignInResultTest.cs new file mode 100644 index 0000000000..ce7a522937 --- /dev/null +++ b/test/Microsoft.AspNet.Identity.Test/SignInResultTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; +using Xunit; + +namespace Microsoft.AspNet.Identity.Test +{ + public class SignInResultTest + { + [Fact] + public void VerifyLogSuccess() + { + var result = SignInResult.Success; + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Result : Succeeded", logMessage.ToString()); + } + + [Fact] + public void VerifyLogLockedOut() + { + var result = SignInResult.LockedOut; + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Result : Lockedout", logMessage.ToString()); + } + + [Fact] + public void VerifyLogNotAllowed() + { + var result = SignInResult.NotAllowed; + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Result : NotAllowed", logMessage.ToString()); + } + + [Fact] + public void VerifyLogRequiresTwoFactor() + { + var result = SignInResult.TwoFactorRequired; + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Result : RequiresTwoFactor", logMessage.ToString()); + } + + [Fact] + public void VerifyLogRequiresFailed() + { + var result = SignInResult.Failed; + var logMessage = new StringBuilder(); + var logger = MockHelpers.MockILogger(logMessage); + + result.Log(logger.Object, "Operation"); + + Assert.Equal("Operation : Result : Failed", logMessage.ToString()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index f9068d6e30..62f1e77917 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.DependencyInjection.Fallback; -using Microsoft.Framework.OptionsModel; -using Moq; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Moq; using Xunit; namespace Microsoft.AspNet.Identity.Test @@ -21,10 +20,7 @@ namespace Microsoft.AspNet.Identity.Test { public IUserStore StorePublic { get { return Store; } } - public TestManager(IUserStore store, IOptions optionsAccessor, - IPasswordHasher passwordHasher, IEnumerable> userValidator, - IEnumerable> passwordValidator) - : base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null, null, null) { } + public TestManager(IUserStore store) : base(store) { } } [Fact] @@ -36,8 +32,6 @@ namespace Microsoft.AspNet.Identity.Test services.AddIdentity(); var manager = services.BuildServiceProvider().GetRequiredService(); Assert.NotNull(manager.PasswordHasher); - Assert.Equal(1, manager.PasswordValidators.Count); - Assert.Equal(1, manager.UserValidators.Count); Assert.NotNull(manager.StorePublic); Assert.NotNull(manager.Options); } @@ -48,7 +42,30 @@ namespace Microsoft.AspNet.Identity.Test // Setup var store = new Mock>(); var user = new TestUser { UserName = "Foo" }; - store.Setup(s => s.CreateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.CreateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.CreateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task CreateCallsUpdateEmailStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo", Email = "Foo@foo.com" }; + store.Setup(s => s.CreateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.Email)).Verifiable(); + store.Setup(s => s.SetNormalizedEmailAsync(user, user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); // Act @@ -65,8 +82,8 @@ namespace Microsoft.AspNet.Identity.Test // Setup var store = new Mock>(); var user = new TestUser { UserName = "Foo" }; - store.Setup(s => s.DeleteAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.DeleteAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.DeleteAsync(user); @@ -82,8 +99,31 @@ namespace Microsoft.AspNet.Identity.Test // Setup var store = new Mock>(); var user = new TestUser { UserName = "Foo" }; - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.UpdateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateWillUpdateNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo", Email = "email" }; + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.Email)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetNormalizedEmailAsync(user, user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.UpdateAsync(user); @@ -99,8 +139,11 @@ namespace Microsoft.AspNet.Identity.Test // Setup var store = new Mock>(); var user = new TestUser(); - store.Setup(s => s.SetUserNameAsync(user, It.IsAny(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.SetUserNameAsync(user, "foo", CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult("foo")).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, "FOO", CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.SetUserNameAsync(user, "foo"); @@ -132,7 +175,7 @@ namespace Microsoft.AspNet.Identity.Test { // Setup var store = new Mock>(); - var user = new TestUser {UserName="Foo"}; + var user = new TestUser { UserName = "Foo" }; store.Setup(s => s.FindByNameAsync(user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); @@ -149,10 +192,10 @@ namespace Microsoft.AspNet.Identity.Test { // Setup var store = new Mock>(); - var user = new TestUser {UserName="Foo"}; + var user = new TestUser { UserName = "Foo" }; store.Setup(s => s.FindByNameAsync(user.UserName, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); - userManager.UserNameNormalizer = null; + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.KeyNormalizer = null; // Act var result = await userManager.FindByNameAsync(user.UserName); @@ -162,13 +205,48 @@ namespace Microsoft.AspNet.Identity.Test store.VerifyAll(); } + [Fact] + public async Task FindByEmailCallsStoreWithNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { Email = "Foo" }; + store.Setup(s => s.FindByEmailAsync(user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.FindByEmailAsync(user.Email); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByEmailCallsStoreWithoutNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { Email = "Foo" }; + store.Setup(s => s.FindByEmailAsync(user.Email, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.KeyNormalizer = null; + + // Act + var result = await userManager.FindByEmailAsync(user.Email); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + [Fact] public async Task AddToRolesCallsStore() { // Setup var store = new Mock>(); var user = new TestUser { UserName = "Foo" }; - var roles = new string[] {"A", "B", "C"}; + var roles = new string[] { "A", "B", "C" }; store.Setup(s => s.AddToRoleAsync(user, "A", CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); @@ -178,7 +256,7 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.AddToRoleAsync(user, "C", CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); store.Setup(s => s.GetRolesAsync(user, CancellationToken.None)).ReturnsAsync(new List()).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); @@ -207,7 +285,7 @@ namespace Microsoft.AspNet.Identity.Test var result = await userManager.AddToRolesAsync(user, roles); // Assert - IdentityResultAssert.IsFailure(result, "User already in role."); + IdentityResultAssert.IsFailure(result, IdentityErrorDescriber.Default.UserAlreadyInRole("B")); store.VerifyAll(); } @@ -227,7 +305,7 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.RemoveFromRoleAsync(user, "C", CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); store.Setup(s => s.IsInRoleAsync(user, "A", CancellationToken.None)) .Returns(Task.FromResult(true)) .Verifiable(); @@ -269,7 +347,7 @@ namespace Microsoft.AspNet.Identity.Test var result = await userManager.RemoveFromRolesAsync(user, roles); // Assert - IdentityResultAssert.IsFailure(result, "User is not in role."); + IdentityResultAssert.IsFailure(result, IdentityErrorDescriber.Default.UserNotInRole("B")); store.VerifyAll(); } @@ -283,8 +361,8 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.AddClaimsAsync(user, claims, CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.AddClaimsAsync(user, claims); @@ -304,8 +382,8 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.AddClaimsAsync(user, It.IsAny>(), CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.AddClaimAsync(user, claim); @@ -326,7 +404,8 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.ReplaceClaimAsync(user, It.IsAny(), It.IsAny(), CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.ReplaceClaimAsync(user, claim, newClaim); @@ -336,6 +415,34 @@ namespace Microsoft.AspNet.Identity.Test store.VerifyAll(); } + [Fact] + public async Task CheckPasswordWillRehashPasswordWhenNeeded() + { + // Setup + var store = new Mock>(); + var hasher = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var pwd = "password"; + var hashed = "hashed"; + var rehashed = "rehashed"; + + store.Setup(s => s.GetPasswordHashAsync(user, CancellationToken.None)) + .ReturnsAsync(hashed) + .Verifiable(); + hasher.Setup(s => s.VerifyHashedPassword(user, hashed, pwd)).Returns(PasswordVerificationResult.SuccessRehashNeeded).Verifiable(); + hasher.Setup(s => s.HashPassword(user, pwd)).Returns(rehashed).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.PasswordHasher = hasher.Object; + + // Act + var result = await userManager.CheckPasswordAsync(user, pwd); + + // Assert + Assert.True(result); + store.VerifyAll(); + hasher.VerifyAll(); + } + [Fact] public async Task RemoveClaimsCallsStore() { @@ -346,8 +453,8 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.RemoveClaimsAsync(user, claims, CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); - var userManager = MockHelpers.TestUserManager(store.Object); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); // Act var result = await userManager.RemoveClaimsAsync(user, claims); @@ -367,7 +474,7 @@ namespace Microsoft.AspNet.Identity.Test store.Setup(s => s.RemoveClaimsAsync(user, It.IsAny>(), CancellationToken.None)) .Returns(Task.FromResult(0)) .Verifiable(); - store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); var userManager = MockHelpers.TestUserManager(store.Object); // Act @@ -533,17 +640,11 @@ namespace Microsoft.AspNet.Identity.Test public async Task ManagerPublicNullChecks() { var store = new NotImplementedStore(); - var optionsAccessor = new OptionsManager(null); - var passwordHasher = new PasswordHasher(new PasswordHasherOptionsAccessor()); Assert.Throws("store", - () => new UserManager(null, null, null, null, null, null, null, null)); - Assert.Throws("optionsAccessor", - () => new UserManager(store, null, null, null, null, null, null, null)); - Assert.Throws("passwordHasher", - () => new UserManager(store, optionsAccessor, null, null, null, null, null, null)); + () => new UserManager(null, null)); - var manager = new UserManager(store, optionsAccessor, passwordHasher, null, null, null, null, null); + var manager = new UserManager(store); Assert.Throws("value", () => manager.PasswordHasher = null); Assert.Throws("value", () => manager.Options = null); @@ -581,7 +682,7 @@ namespace Microsoft.AspNet.Identity.Test await Assert.ThrowsAsync("user", async () => await manager.AddClaimAsync(null, new Claim("a", "b"))); await Assert.ThrowsAsync("user", - async () => await manager.AddLoginAsync(null, new UserLoginInfo("","",""))); + async () => await manager.AddLoginAsync(null, new UserLoginInfo("", "", ""))); await Assert.ThrowsAsync("user", async () => await manager.AddPasswordAsync(null, null)); await Assert.ThrowsAsync("user", @@ -714,7 +815,7 @@ namespace Microsoft.AspNet.Identity.Test private class BadPasswordValidator : IPasswordValidator where TUser : class { - public const string ErrorMessage = "I'm Bad."; + public static readonly IdentityError ErrorMessage = new IdentityError { Description = "I'm Bad." }; public Task ValidateAsync(UserManager manager, TUser user, string password, CancellationToken cancellationToken = default(CancellationToken)) { @@ -842,19 +943,19 @@ namespace Microsoft.AspNet.Identity.Test return Task.FromResult(0); } - public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } - public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } - public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(0); + return Task.FromResult(IdentityResult.Success); } public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) @@ -961,6 +1062,26 @@ namespace Microsoft.AspNet.Identity.Test { return Task.FromResult(0); } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task GetNormalizedEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(""); + } + + public Task SetNormalizedEmailAsync(TestUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } } private class NoOpTokenProvider : IUserTokenProvider @@ -1119,21 +1240,6 @@ namespace Microsoft.AspNet.Identity.Test throw new NotImplementedException(); } - public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - throw new NotImplementedException(); - } - - public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - throw new NotImplementedException(); - } - - public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) - { - throw new NotImplementedException(); - } - public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) { throw new NotImplementedException(); @@ -1218,6 +1324,87 @@ namespace Microsoft.AspNet.Identity.Test { throw new NotImplementedException(); } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + Task IUserStore.CreateAsync(TestUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IUserStore.UpdateAsync(TestUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IUserStore.DeleteAsync(TestUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedEmailAsync(TestUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } } + + [Fact] + public async Task CanCustomizeUserValidatorErrors() + { + var services = new ServiceCollection(); + var store = new Mock>(); + var describer = new TestErrorDescriber(); + services.AddInstance(describer) + .AddInstance>(store.Object) + .AddIdentity(); + + var manager = services.BuildServiceProvider().GetRequiredService>(); + + manager.Options.User.RequireUniqueEmail = true; + var user = new TestUser() { UserName = "dupeEmail", Email = "dupe@email.com" }; + var user2 = new TestUser() { UserName = "dupeEmail2", Email = "dupe@email.com" }; + store.Setup(s => s.FindByEmailAsync("DUPE@EMAIL.COM", CancellationToken.None)) + .Returns(Task.FromResult(user2)) + .Verifiable(); + store.Setup(s => s.GetUserIdAsync(user2, CancellationToken.None)) + .Returns(Task.FromResult(user2.Id)) + .Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)) + .Returns(Task.FromResult(user.UserName)) + .Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)) + .Returns(Task.FromResult(user.Email)) + .Verifiable(); + + Assert.Same(describer, manager.ErrorDescriber); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), describer.DuplicateEmail(user.Email)); + + store.VerifyAll(); + } + + public class TestErrorDescriber : IdentityErrorDescriber + { + public static string Code = "Error"; + public static string FormatError = "FormatError {0}"; + + public override IdentityError DuplicateEmail(string email) + { + return new IdentityError { Code = Code, Description = string.Format(FormatError, email) }; + } + } + } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/UserValidatorTest.cs b/test/Microsoft.AspNet.Identity.Test/UserValidatorTest.cs index 793dd36598..093ecb5131 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserValidatorTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserValidatorTest.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Identity.Test var result = await validator.ValidateAsync(manager, user); // Assert - IdentityResultAssert.IsFailure(result, "UserName cannot be null or empty."); + IdentityResultAssert.IsFailure(result, IdentityErrorDescriber.Default.InvalidUserName(input)); } [Theory] diff --git a/test/Microsoft.AspNet.Identity.Test/project.json b/test/Microsoft.AspNet.Identity.Test/project.json index 75d6ba15c1..60d12687be 100644 --- a/test/Microsoft.AspNet.Identity.Test/project.json +++ b/test/Microsoft.AspNet.Identity.Test/project.json @@ -3,13 +3,12 @@ "Microsoft.AspNet.Hosting" : "1.0.0-*", "Microsoft.AspNet.Http" : "1.0.0-*", "Microsoft.AspNet.Identity" : "3.0.0-*", - "Microsoft.AspNet.PipelineCore" : "1.0.0-*", "Microsoft.AspNet.RequestContainer" : "1.0.0-*", "Microsoft.AspNet.Testing" : "1.0.0-*", "Microsoft.Framework.ConfigurationModel" : "1.0.0-*", "Microsoft.Framework.DependencyInjection" : "1.0.0-*", "Microsoft.Framework.OptionsModel" : "1.0.0-*", - "Xunit.KRunner": "1.0.0-*" + "xunit.runner.kre": "1.0.0-*" }, "code": "**\\*.cs;..\\Shared\\*.cs", "frameworks": { @@ -20,6 +19,6 @@ } }, "commands": { - "test": "Xunit.KRunner" + "test": "xunit.runner.kre" } } diff --git a/test/Shared/IdentityResultAssert.cs b/test/Shared/IdentityResultAssert.cs index eb92499bf1..80fabb9c02 100644 --- a/test/Shared/IdentityResultAssert.cs +++ b/test/Shared/IdentityResultAssert.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using Microsoft.Framework.Logging; using Xunit; namespace Microsoft.AspNet.Identity.Test @@ -24,7 +25,79 @@ namespace Microsoft.AspNet.Identity.Test { Assert.NotNull(result); Assert.False(result.Succeeded); - Assert.Equal(error, result.Errors.First()); + Assert.Equal(error, result.Errors.First().Description); + } + + public static void IsFailure(IdentityResult result, IdentityError error) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + Assert.Equal(error.Description, result.Errors.First().Description); + Assert.Equal(error.Code, result.Errors.First().Code); + } + + public static void VerifyUserManagerFailureLog(ILogger logger, string methodName, string userId, params IdentityError[] errors) + { + VerifyFailureLog(logger, "UserManager", methodName, userId, "user", errors); + } + + public static void VerifyRoleManagerFailureLog(ILogger logger, string methodName, string roleId, params IdentityError[] errors) + { + VerifyFailureLog(logger, "RoleManager", methodName, roleId, "role", errors); + } + + public static void VerifyUserManagerSuccessLog(ILogger logger, string methodName, string userId) + { + VerifySuccessLog(logger, "UserManager", methodName, userId, "user"); + + } + + public static void VerifyRoleManagerSuccessLog(ILogger logger, string methodName, string roleId) + { + VerifySuccessLog(logger, "RoleManager", methodName, roleId, "role"); + + } + private static void VerifySuccessLog(ILogger logger, string className, string methodName, string id, string userOrRole = "user") + { + TestLogger testlogger = logger as TestLogger; + if (testlogger != null) + { + string expected = string.Format("{0} for {1}: {2} : Success", methodName, userOrRole, id); + Assert.True(testlogger.LogMessages.Contains(expected)); + } + else + { + Assert.True(true, "No logger registered"); + } + } + + public static void VerifyLogMessage(ILogger logger, string expectedLog) + { + TestLogger testlogger = logger as TestLogger; + if (testlogger != null) + { + Assert.True(testlogger.LogMessages.Contains(expectedLog)); + } + else + { + Assert.True(true, "No logger registered"); + } + } + + private static void VerifyFailureLog(ILogger logger, string className, string methodName, string userId, string userOrRole = "user", params IdentityError[] errors) + { + TestLogger testlogger = logger as TestLogger; + if (testlogger != null) + { + errors = errors ?? new IdentityError[] { new IdentityError() }; + string expected = string.Format("{0} for {1}: {2} : Failed : {3}", methodName, userOrRole, userId, string.Join(",", errors.Select(x => x.Code).ToList())); + + Assert.True(testlogger.LogMessages.Contains(expected)); + } + else + { + Assert.True(true, "No logger registered"); + } } } } \ No newline at end of file diff --git a/test/Shared/MockHelpers.cs b/test/Shared/MockHelpers.cs index 87d113c96a..36f2690c60 100644 --- a/test/Shared/MockHelpers.cs +++ b/test/Shared/MockHelpers.cs @@ -1,59 +1,86 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Threading; using System.Threading.Tasks; -using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Logging; using Moq; +using System.Text; namespace Microsoft.AspNet.Identity.Test { public static class MockHelpers { + public static StringBuilder LogMessage = new StringBuilder(); + public static Mock> MockUserManager() where TUser : class { var store = new Mock>(); - var options = new OptionsManager(null); - var userValidators = new List>(); - userValidators.Add(new UserValidator()); - var pwdValidators = new List>(); - pwdValidators.Add(new PasswordValidator()); - return new Mock>( - store.Object, - options, - new PasswordHasher(new PasswordHasherOptionsAccessor()), - userValidators, - pwdValidators, - new UpperInvariantUserNameNormalizer(), - new List>(), - new List()); + var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null, null); + mgr.Object.UserValidators.Add(new UserValidator()); + mgr.Object.PasswordValidators.Add(new PasswordValidator()); + return mgr; } - public static Mock> MockRoleManager() where TRole : class + public static Mock> MockRoleManager(IRoleStore store = null) where TRole : class { - var store = new Mock>(); + store = store ?? new Mock>().Object; var roles = new List>(); roles.Add(new RoleValidator()); - return new Mock>(store.Object, roles); + return new Mock>(store, roles, null, null,null); } - public static UserManager TestUserManager() where TUser : class + public static Mock MockILogger(StringBuilder logStore = null) { - return TestUserManager(new Mock>().Object); + logStore = logStore ?? LogMessage; + var logger = new Mock(); + logger.Setup(x => x.Write(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())) + .Callback((LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) => + { logStore.Append(state.ToString()); }); + logger.Setup(x => x.IsEnabled(LogLevel.Information)).Returns(true); + logger.Setup(x => x.IsEnabled(LogLevel.Warning)).Returns(true); + + return logger; } - public static UserManager TestUserManager(IUserStore store) where TUser : class + public static Mock MockILoggerFactory(ILogger logger = null) { - var options = new OptionsManager(null); - var validator = new Mock>(); - var userManager = new UserManager(store, options, new PasswordHasher(new PasswordHasherOptionsAccessor()), - null, null, new UpperInvariantUserNameNormalizer(), null, null); + logger = logger ?? MockILogger().Object; + var loggerFactory = new Mock(); + loggerFactory.Setup(x => x.Create(It.IsAny())).Returns(logger); + return loggerFactory; + } + + public static UserManager UserManagerWithMockLogger(ILoggerFactory loggerFactory = null) where TUser : class + { + var userstore = new Mock>().Object; + var userManager = new UserManager(userstore, loggerFactory: loggerFactory ?? MockILoggerFactory().Object); + + return userManager; + } + + public static UserManager TestUserManager(IUserStore store = null) where TUser : class + { + store = store ?? new Mock>().Object; + var validator = new Mock>(); + var userManager = new UserManager(store); userManager.UserValidators.Add(validator.Object); userManager.PasswordValidators.Add(new PasswordValidator()); validator.Setup(v => v.ValidateAsync(userManager, It.IsAny(), CancellationToken.None)) .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); return userManager; } + + public static RoleManager TestRoleManager(IRoleStore store = null) where TRole : class + { + store = store ?? new Mock>().Object; + var roles = new List>(); + roles.Add(new RoleValidator()); + return new RoleManager(store, roles); + } + } } \ No newline at end of file diff --git a/test/Shared/PriorityOrderer.cs b/test/Shared/PriorityOrderer.cs index d9161de20f..8702e415d8 100644 --- a/test/Shared/PriorityOrderer.cs +++ b/test/Shared/PriorityOrderer.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Identity.Test { int priority = 0; - foreach (IAttributeInfo attr in testCase.Method.GetCustomAttributes((typeof(TestPriorityAttribute)))) + foreach (IAttributeInfo attr in testCase.TestMethod.Method.GetCustomAttributes((typeof(TestPriorityAttribute)))) priority = attr.GetNamedArgument("Priority"); GetOrCreate(sortedMethods, priority).Add(testCase); @@ -38,7 +38,7 @@ namespace Microsoft.AspNet.Identity.Test foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority])) { - list.Sort((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.Method.Name, y.Method.Name)); + list.Sort((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.TestMethod.Method.Name, y.TestMethod.Method.Name)); foreach (XunitTestCase testCase in list) yield return testCase; } diff --git a/test/Shared/TestLogger.cs b/test/Shared/TestLogger.cs new file mode 100644 index 0000000000..7cd1db5d86 --- /dev/null +++ b/test/Shared/TestLogger.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Identity.Test +{ + public class TestLogger : ILogger + { + public IList LogMessages { get; private set; } = new List(); + + public IDisposable BeginScope(object state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Write(LogLevel logLevel, int eventId, object state, Exception exception, Func formatter) + { + LogMessages.Add(state.ToString()); + } + } +} \ No newline at end of file diff --git a/test/Shared/TestLoggerFactory.cs b/test/Shared/TestLoggerFactory.cs new file mode 100644 index 0000000000..5d03765a57 --- /dev/null +++ b/test/Shared/TestLoggerFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Identity.Test +{ + public class TestLoggerFactory : ILoggerFactory + { + public void AddProvider(ILoggerProvider provider) + { + + } + + public ILogger Create(string name) + { + return new TestLogger(); + } + } +} diff --git a/test/Shared/TestUser.cs b/test/Shared/TestUser.cs index 19b7e391f4..40436a8361 100644 --- a/test/Shared/TestUser.cs +++ b/test/Shared/TestUser.cs @@ -14,5 +14,6 @@ namespace Microsoft.AspNet.Identity.Test public string Id { get; private set; } public string UserName { get; set; } + public string Email { get; set; } } } \ No newline at end of file diff --git a/test/Shared/UserManagerTestBase.cs b/test/Shared/UserManagerTestBase.cs index 2ba65a828a..a4d1e0b4ce 100644 --- a/test/Shared/UserManagerTestBase.cs +++ b/test/Shared/UserManagerTestBase.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Testing; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.Logging; using Xunit; namespace Microsoft.AspNet.Identity.Test @@ -20,17 +21,25 @@ namespace Microsoft.AspNet.Identity.Test where TRole : IdentityRole, new() { } - public abstract class UserManagerTestBase - where TUser: IdentityUser, new() - where TRole: IdentityRole, new() + public abstract class UserManagerTestBase + where TUser : IdentityUser, new() + where TRole : IdentityRole, new() where TKey : IEquatable { + protected TestLoggerFactory loggerFactory; + + public UserManagerTestBase() + { + loggerFactory = new TestLoggerFactory(); + } + protected virtual void SetupIdentityServices(IServiceCollection services, object context = null) { services.AddHosting(); services.AddIdentity().AddDefaultTokenProviders(); AddUserStore(services, context); AddRoleStore(services, context); + services.AddInstance(loggerFactory); services.ConfigureIdentity(options => { options.Password.RequireDigit = false; @@ -74,11 +83,13 @@ namespace Microsoft.AspNet.Identity.Test protected abstract void AddUserStore(IServiceCollection services, object context = null); protected abstract void AddRoleStore(IServiceCollection services, object context = null); - protected TUser CreateTestUser(string namePrefix = "") { + protected TUser CreateTestUser(string namePrefix = "") + { return new TUser() { UserName = namePrefix + Guid.NewGuid().ToString() }; } - protected TRole CreateRole(string namePrefix = "") { + protected TRole CreateRole(string namePrefix = "") + { return new TRole() { Name = namePrefix + Guid.NewGuid().ToString() }; } @@ -88,7 +99,11 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var user = CreateTestUser(); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "CreateAsync", user.Id.ToString()); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "DeleteAsync", user.Id.ToString()); + Assert.Null(await manager.FindByIdAsync(user.Id.ToString())); } @@ -103,6 +118,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Null(await manager.FindByNameAsync(newName)); user.UserName = newName; IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "UpdateAsync", user.Id.ToString()); Assert.NotNull(await manager.FindByNameAsync(newName)); Assert.Null(await manager.FindByNameAsync(name)); } @@ -126,9 +142,14 @@ namespace Microsoft.AspNet.Identity.Test var user = new TUser() { UserName = "UpdatePassword" }; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); Assert.True(await manager.CheckPasswordAsync(user, "password")); + string expectedLog = string.Format("{0} for user: {1} : {2}", "CheckPasswordAsync", user.Id.ToString(), true.ToString()); + IdentityResultAssert.VerifyLogMessage(manager.Logger, expectedLog); + user.PasswordHash = manager.PasswordHasher.HashPassword(user, "New"); IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user)); Assert.False(await manager.CheckPasswordAsync(user, "password")); + expectedLog = string.Format("{0} for user: {1} : {2}", "CheckPasswordAsync", user.Id.ToString(), false.ToString()); + IdentityResultAssert.VerifyLogMessage(manager.Logger, expectedLog); Assert.True(await manager.CheckPasswordAsync(user, "New")); } @@ -160,6 +181,7 @@ namespace Microsoft.AspNet.Identity.Test manager.UserValidators.Clear(); manager.UserValidators.Add(new AlwaysBadValidator()); IdentityResultAssert.IsFailure(await manager.UpdateAsync(user), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "UpdateAsync", user.Id.ToString(), AlwaysBadValidator.ErrorMessage); } [Fact] @@ -182,7 +204,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var user = CreateTestUser(); manager.Options.User.RequireUniqueEmail = true; - IdentityResultAssert.IsFailure(await manager.CreateAsync(user), "Email cannot be null or empty."); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), IdentityErrorDescriber.Default.InvalidEmail(email)); } #if ASPNET50 @@ -194,7 +216,7 @@ namespace Microsoft.AspNet.Identity.Test var manager = CreateManager(); var user = new TUser() { UserName = "UpdateBlocked", Email = email }; manager.Options.User.RequireUniqueEmail = true; - IdentityResultAssert.IsFailure(await manager.CreateAsync(user), "Email '" + email + "' is invalid."); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), IdentityErrorDescriber.Default.InvalidEmail(email)); } #endif @@ -208,6 +230,7 @@ namespace Microsoft.AspNet.Identity.Test manager.PasswordValidators.Add(new AlwaysBadValidator()); IdentityResultAssert.IsFailure(await manager.AddPasswordAsync(user, "password"), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "AddPasswordAsync", user.Id.ToString(), AlwaysBadValidator.ErrorMessage); } [Fact] @@ -234,6 +257,17 @@ namespace Microsoft.AspNet.Identity.Test manager.PasswordValidators.Add(new AlwaysBadValidator()); IdentityResultAssert.IsFailure(await manager.ChangePasswordAsync(user, "password", "new"), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ChangePasswordAsync", user.Id.ToString(), AlwaysBadValidator.ErrorMessage); + } + + [Fact] + public async Task PasswordValidatorCanBlockCreateUser() + { + var manager = CreateManager(); + var user = CreateTestUser(); + manager.PasswordValidators.Clear(); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user, "password"), AlwaysBadValidator.ErrorMessage); } [Fact] @@ -260,6 +294,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); user = await manager.FindByNameAsync(user.UserName); IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, new UserLoginInfo(provider, providerKey, display))); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "AddLoginAsync", user.Id.ToString()); var logins = await manager.GetLoginsAsync(user); Assert.NotNull(logins); Assert.Equal(1, logins.Count()); @@ -278,6 +313,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); Assert.False(await manager.HasPasswordAsync(user)); IdentityResultAssert.IsSuccess(await manager.AddPasswordAsync(user, "password")); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "AddPasswordAsync", user.Id.ToString()); Assert.True(await manager.HasPasswordAsync(user)); var logins = await manager.GetLoginsAsync(user); Assert.NotNull(logins); @@ -295,6 +331,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.True(await manager.HasPasswordAsync(user)); IdentityResultAssert.IsFailure(await manager.AddPasswordAsync(user, "password"), "User already has a password set."); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "AddPasswordAsync", user.Id.ToString(), IdentityErrorDescriber.Default.UserAlreadyHasPassword()); } [Fact] @@ -316,6 +353,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Equal(login.ProviderDisplayName, logins.Last().ProviderDisplayName); var stamp = user.SecurityStamp; IdentityResultAssert.IsSuccess(await manager.RemoveLoginAsync(user, login.LoginProvider, login.ProviderKey)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "RemoveLoginAsync", user.Id.ToString()); Assert.Null(await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)); logins = await manager.GetLoginsAsync(user); Assert.NotNull(logins); @@ -332,6 +370,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); var stamp = user.SecurityStamp; IdentityResultAssert.IsSuccess(await manager.RemovePasswordAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "RemovePasswordAsync", user.Id.ToString()); var u = await manager.FindByNameAsync(user.UserName); Assert.NotNull(u); Assert.Null(u.PasswordHash); @@ -351,6 +390,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, password, newPassword)); Assert.False(await manager.CheckPasswordAsync(user, password)); Assert.True(await manager.CheckPasswordAsync(user, newPassword)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "ChangePasswordAsync", user.Id.ToString()); Assert.NotEqual(stamp, user.SecurityStamp); } @@ -365,6 +405,35 @@ namespace Microsoft.AspNet.Identity.Test { IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); } + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "AddClaimsAsync", user.Id.ToString()); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(3, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0])); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "RemoveClaimsAsync", user.Id.ToString()); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(2, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(0, userClaims.Count); + } + + [Fact] + public async Task RemoveClaimOnlyAffectsUser() + { + var manager = CreateManager(); + var user = CreateTestUser(); + var user2 = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + Claim[] claims = { new Claim("c", "v"), new Claim("c2", "v2"), new Claim("c2", "v3") }; + foreach (Claim c in claims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user2, c)); + } var userClaims = await manager.GetClaimsAsync(user); Assert.Equal(3, userClaims.Count); IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0])); @@ -376,6 +445,8 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2])); userClaims = await manager.GetClaimsAsync(user); Assert.Equal(0, userClaims.Count); + var userClaims2 = await manager.GetClaimsAsync(user2); + Assert.Equal(3, userClaims2.Count); } [Fact] @@ -390,6 +461,7 @@ namespace Microsoft.AspNet.Identity.Test Claim claim = new Claim("c", "b"); Claim oldClaim = userClaims.FirstOrDefault(); IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "ReplaceClaimAsync", user.Id.ToString()); var newUserClaims = await manager.GetClaimsAsync(user); Assert.Equal(1, newUserClaims.Count); Claim newClaim = newUserClaims.FirstOrDefault(); @@ -397,6 +469,35 @@ namespace Microsoft.AspNet.Identity.Test Assert.Equal(claim.Value, newClaim.Value); } + [Fact] + public async Task ReplaceUserClaimOnlyAffectsUser() + { + var manager = CreateManager(); + var user = CreateTestUser(); + var user2 = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("c", "a"))); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user2, new Claim("c", "a"))); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + var userClaims2 = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims2.Count); + Claim claim = new Claim("c", "b"); + Claim oldClaim = userClaims.FirstOrDefault(); + IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim)); + var newUserClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, newUserClaims.Count); + Claim newClaim = newUserClaims.FirstOrDefault(); + Assert.Equal(claim.Type, newClaim.Type); + Assert.Equal(claim.Value, newClaim.Value); + userClaims2 = await manager.GetClaimsAsync(user2); + Assert.Equal(1, userClaims2.Count); + Claim oldClaim2 = userClaims2.FirstOrDefault(); + Assert.Equal("c", oldClaim2.Type); + Assert.Equal("a", oldClaim2.Value); + } + [Fact] public async Task ChangePasswordFallsIfPasswordWrong() { @@ -405,6 +506,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); var result = await manager.ChangePasswordAsync(user, "bogus", "newpassword"); IdentityResultAssert.IsFailure(result, "Incorrect password."); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ChangePasswordAsync", user.Id.ToString(), IdentityErrorDescriber.Default.PasswordMismatch()); } [Fact] @@ -414,7 +516,7 @@ namespace Microsoft.AspNet.Identity.Test var user = CreateTestUser(); var user2 = new TUser() { UserName = user.UserName }; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - IdentityResultAssert.IsFailure(await manager.CreateAsync(user2), "Name "+user.UserName+" is already taken."); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user2), IdentityErrorDescriber.Default.DuplicateUserName(user.UserName)); } [Fact] @@ -440,7 +542,7 @@ namespace Microsoft.AspNet.Identity.Test user.Email = email; user2.Email = email; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); - IdentityResultAssert.IsFailure(await manager.CreateAsync(user2), "Email '"+email+"' is already taken."); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user2), IdentityErrorDescriber.Default.DuplicateEmail(user.Email)); } [Fact] @@ -453,6 +555,7 @@ namespace Microsoft.AspNet.Identity.Test var stamp = user.SecurityStamp; Assert.NotNull(stamp); IdentityResultAssert.IsSuccess(await manager.UpdateSecurityStampAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "UpdateSecurityStampAsync", user.Id.ToString()); Assert.NotEqual(stamp, user.SecurityStamp); } @@ -465,7 +568,8 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); var result = await manager.AddLoginAsync(user, login); - IdentityResultAssert.IsFailure(result, "A user with that external login already exists."); + IdentityResultAssert.IsFailure(result, IdentityErrorDescriber.Default.LoginAlreadyAssociated()); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "AddLoginAsync", user.Id.ToString(), IdentityErrorDescriber.Default.LoginAlreadyAssociated()); } // Email tests @@ -549,7 +653,9 @@ namespace Microsoft.AspNet.Identity.Test Assert.NotNull(stamp); var token = await manager.GeneratePasswordResetTokenAsync(user); Assert.NotNull(token); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "GeneratePasswordResetTokenAsync", user.Id.ToString()); IdentityResultAssert.IsSuccess(await manager.ResetPasswordAsync(user, token, newPassword)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "ResetPasswordAsync", user.Id.ToString()); Assert.False(await manager.CheckPasswordAsync(user, password)); Assert.True(await manager.CheckPasswordAsync(user, newPassword)); Assert.NotEqual(stamp, user.SecurityStamp); @@ -572,6 +678,7 @@ namespace Microsoft.AspNet.Identity.Test manager.PasswordValidators.Add(new AlwaysBadValidator()); IdentityResultAssert.IsFailure(await manager.ResetPasswordAsync(user, token, newPassword), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ResetPasswordAsync", user.Id.ToString(), AlwaysBadValidator.ErrorMessage); Assert.True(await manager.CheckPasswordAsync(user, password)); Assert.Equal(stamp, user.SecurityStamp); } @@ -589,6 +696,7 @@ namespace Microsoft.AspNet.Identity.Test var stamp = user.SecurityStamp; Assert.NotNull(stamp); IdentityResultAssert.IsFailure(await manager.ResetPasswordAsync(user, "bogus", newPassword), "Invalid token."); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ResetPasswordAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); Assert.True(await manager.CheckPasswordAsync(user, password)); Assert.Equal(stamp, user.SecurityStamp); } @@ -603,8 +711,14 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); var token = await manager.GenerateUserTokenAsync(user, "Static", "test"); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "GenerateUserTokenAsync", user.Id.ToString()); + Assert.True(await manager.VerifyUserTokenAsync(user, "Static", "test", token)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "VerifyUserTokenAsync", user.Id.ToString()); + Assert.False(await manager.VerifyUserTokenAsync(user, "Static", "test2", token)); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "VerifyUserTokenAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); + Assert.False(await manager.VerifyUserTokenAsync(user, "Static", "test", token + "a")); Assert.False(await manager.VerifyUserTokenAsync(user2, "Static", "test", token)); } @@ -620,7 +734,9 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); var token = await manager.GenerateEmailConfirmationTokenAsync(user); Assert.NotNull(token); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "GenerateEmailConfirmationTokenAsync", user.Id.ToString()); IdentityResultAssert.IsSuccess(await manager.ConfirmEmailAsync(user, token)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "ConfirmEmailAsync", user.Id.ToString()); Assert.True(await manager.IsEmailConfirmedAsync(user)); IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, null)); Assert.False(await manager.IsEmailConfirmedAsync(user)); @@ -637,6 +753,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); IdentityResultAssert.IsFailure(await manager.ConfirmEmailAsync(user, "bogus"), "Invalid token."); Assert.False(await manager.IsEmailConfirmedAsync(user)); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ConfirmEmailAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); } [Fact] @@ -712,6 +829,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); Assert.Equal(1, await mgr.GetAccessFailedCountAsync(user)); IdentityResultAssert.IsSuccess(await mgr.ResetAccessFailedCountAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(mgr.Logger, "ResetAccessFailedCountAsync", user.Id.ToString()); Assert.Equal(0, await mgr.GetAccessFailedCountAsync(user)); Assert.False(await mgr.IsLockedOutAsync(user)); Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); @@ -734,8 +852,10 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await mgr.SetLockoutEnabledAsync(user, true)); Assert.True(await mgr.GetLockoutEnabledAsync(user)); Assert.True(user.LockoutEnabled); + IdentityResultAssert.VerifyUserManagerSuccessLog(mgr.Logger, "SetLockoutEnabledAsync", user.Id.ToString()); Assert.False(await mgr.IsLockedOutAsync(user)); IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(mgr.Logger, "AccessFailedAsync", user.Id.ToString()); Assert.False(await mgr.IsLockedOutAsync(user)); Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); Assert.Equal(1, await mgr.GetAccessFailedCountAsync(user)); @@ -756,6 +876,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.True(user.LockoutEnabled); IdentityResultAssert.IsSuccess(await mgr.SetLockoutEndDateAsync(user, new DateTimeOffset())); Assert.False(await mgr.IsLockedOutAsync(user)); + IdentityResultAssert.VerifyUserManagerSuccessLog(mgr.Logger, "SetLockoutEndDateAsync", user.Id.ToString()); Assert.Equal(new DateTimeOffset(), await mgr.GetLockoutEndDateAsync(user)); Assert.Equal(new DateTimeOffset(), user.LockoutEnd); } @@ -842,7 +963,7 @@ namespace Microsoft.AspNet.Identity.Test private class AlwaysBadValidator : IUserValidator, IRoleValidator, IPasswordValidator { - public const string ErrorMessage = "I'm Bad."; + public static readonly IdentityError ErrorMessage = new IdentityError { Description = "I'm Bad.", Code = "BadValidator" }; public Task ValidateAsync(UserManager manager, TUser user, string password, CancellationToken cancellationToken = default(CancellationToken)) { @@ -892,6 +1013,7 @@ namespace Microsoft.AspNet.Identity.Test manager.RoleValidators.Clear(); manager.RoleValidators.Add(new AlwaysBadValidator()); IdentityResultAssert.IsFailure(await manager.UpdateAsync(role), error); + IdentityResultAssert.VerifyRoleManagerFailureLog(manager.Logger, "UpdateAsync", role.Id.ToString(), AlwaysBadValidator.ErrorMessage); } [Fact] @@ -903,6 +1025,7 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); IdentityResultAssert.IsSuccess(await manager.DeleteAsync(role)); Assert.False(await manager.RoleExistsAsync(role.Name)); + IdentityResultAssert.VerifyRoleManagerSuccessLog(manager.Logger, "DeleteAsync", role.Id.ToString()); } [Fact] @@ -916,6 +1039,7 @@ namespace Microsoft.AspNet.Identity.Test { IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(role, c)); } + IdentityResultAssert.VerifyRoleManagerSuccessLog(manager.Logger, "AddClaimAsync", role.Id.ToString()); var roleClaims = await manager.GetClaimsAsync(role); Assert.Equal(3, roleClaims.Count); IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(role, claims[0])); @@ -927,6 +1051,8 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(role, claims[2])); roleClaims = await manager.GetClaimsAsync(role); Assert.Equal(0, roleClaims.Count); + + IdentityResultAssert.VerifyRoleManagerSuccessLog(manager.Logger, "RemoveClaimAsync", role.Id.ToString()); } [Fact] @@ -958,10 +1084,11 @@ namespace Microsoft.AspNet.Identity.Test Assert.False(await manager.RoleExistsAsync(role.Name)); IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); Assert.True(await manager.RoleExistsAsync(role.Name)); - role.Name = "Changed"; + IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, "Changed")); IdentityResultAssert.IsSuccess(await manager.UpdateAsync(role)); + IdentityResultAssert.VerifyRoleManagerSuccessLog(manager.Logger, "UpdateAsync", role.Id.ToString()); Assert.False(await manager.RoleExistsAsync("update")); - Assert.Equal(role, await manager.FindByNameAsync(role.Name)); + Assert.Equal(role, await manager.FindByNameAsync("Changed")); } [Fact] @@ -1027,6 +1154,7 @@ namespace Microsoft.AspNet.Identity.Test var role = CreateRole("dupeRole"); Assert.False(await manager.RoleExistsAsync(role.Name)); IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + IdentityResultAssert.VerifyRoleManagerSuccessLog(manager.Logger, "CreateAsync", role.Id.ToString()); Assert.True(await manager.RoleExistsAsync(role.Name)); var role2 = CreateRole(); role2.Name = role.Name; @@ -1051,6 +1179,8 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await manager.CreateAsync(u)); IdentityResultAssert.IsSuccess(await manager.AddToRoleAsync(u, role.Name)); Assert.True(await manager.IsInRoleAsync(u, role.Name)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "AddToRoleAsync", u.Id.ToString()); + } } @@ -1103,6 +1233,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.True(await userManager.IsInRoleAsync(user, r.Name)); } IdentityResultAssert.IsSuccess(await userManager.RemoveFromRoleAsync(user, roles[2].Name)); + IdentityResultAssert.VerifyUserManagerSuccessLog(userManager.Logger, "RemoveFromRoleAsync", user.Id.ToString()); Assert.False(await userManager.IsInRoleAsync(user, roles[2].Name)); } @@ -1142,7 +1273,8 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); var result = await userMgr.RemoveFromRoleAsync(user, role.Name); - IdentityResultAssert.IsFailure(result, "User is not in role."); + IdentityResultAssert.IsFailure(result, IdentityErrorDescriber.Default.UserNotInRole(role.Name)); + IdentityResultAssert.VerifyUserManagerFailureLog(userMgr.Logger, "RemoveFromRoleAsync", user.Id.ToString(), IdentityErrorDescriber.Default.UserNotInRole(role.Name)); } [Fact] @@ -1157,7 +1289,8 @@ namespace Microsoft.AspNet.Identity.Test IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); IdentityResultAssert.IsSuccess(await userMgr.AddToRoleAsync(user, role.Name)); Assert.True(await userMgr.IsInRoleAsync(user, role.Name)); - IdentityResultAssert.IsFailure(await userMgr.AddToRoleAsync(user, role.Name), "User already in role."); + IdentityResultAssert.IsFailure(await userMgr.AddToRoleAsync(user, role.Name), IdentityErrorDescriber.Default.UserAlreadyInRole(role.Name)); + IdentityResultAssert.VerifyUserManagerFailureLog(userMgr.Logger, "AddToRoleAsync", user.Id.ToString(), IdentityErrorDescriber.Default.UserAlreadyInRole(role.Name)); } [Fact] @@ -1203,6 +1336,7 @@ namespace Microsoft.AspNet.Identity.Test var stamp = await manager.GetSecurityStampAsync(user); var token1 = await manager.GenerateChangePhoneNumberTokenAsync(user, "111-111-1111"); IdentityResultAssert.IsSuccess(await manager.ChangePhoneNumberAsync(user, "111-111-1111", token1)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "ChangePhoneNumberAsync", user.Id.ToString()); Assert.True(await manager.IsPhoneNumberConfirmedAsync(user)); Assert.Equal(await manager.GetPhoneNumberAsync(user), "111-111-1111"); Assert.NotEqual(stamp, user.SecurityStamp); @@ -1219,6 +1353,7 @@ namespace Microsoft.AspNet.Identity.Test var stamp = await manager.GetSecurityStampAsync(user); IdentityResultAssert.IsFailure(await manager.ChangePhoneNumberAsync(user, "111-111-1111", "bogus"), "Invalid token."); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ChangePhoneNumberAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); Assert.False(await manager.IsPhoneNumberConfirmedAsync(user)); Assert.Equal(await manager.GetPhoneNumberAsync(user), "123-456-7890"); Assert.Equal(stamp, user.SecurityStamp); @@ -1250,12 +1385,16 @@ namespace Microsoft.AspNet.Identity.Test const string num1 = "111-123-4567"; const string num2 = "111-111-1111"; var token1 = await manager.GenerateChangePhoneNumberTokenAsync(user, num1); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "GenerateChangePhoneNumberTokenAsync", user.Id.ToString()); + var token2 = await manager.GenerateChangePhoneNumberTokenAsync(user, num2); Assert.NotEqual(token1, token2); Assert.True(await manager.VerifyChangePhoneNumberTokenAsync(user, token1, num1)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "VerifyChangePhoneNumberTokenAsync", user.Id.ToString()); Assert.True(await manager.VerifyChangePhoneNumberTokenAsync(user, token2, num2)); Assert.False(await manager.VerifyChangePhoneNumberTokenAsync(user, token2, num1)); Assert.False(await manager.VerifyChangePhoneNumberTokenAsync(user, token1, num2)); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "VerifyChangePhoneNumberTokenAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); } [Fact] @@ -1270,6 +1409,7 @@ namespace Microsoft.AspNet.Identity.Test string newEmail = user.UserName + "@en.vec"; var token1 = await manager.GenerateChangeEmailTokenAsync(user, newEmail); IdentityResultAssert.IsSuccess(await manager.ChangeEmailAsync(user, newEmail, token1)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "ChangeEmailAsync", user.Id.ToString()); Assert.True(await manager.IsEmailConfirmedAsync(user)); Assert.Equal(await manager.GetEmailAsync(user), newEmail); Assert.NotEqual(stamp, user.SecurityStamp); @@ -1287,6 +1427,7 @@ namespace Microsoft.AspNet.Identity.Test var stamp = await manager.GetSecurityStampAsync(user); IdentityResultAssert.IsFailure(await manager.ChangeEmailAsync(user, "whatevah@foo.barf", "bogus"), "Invalid token."); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "ChangeEmailAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); Assert.False(await manager.IsEmailConfirmedAsync(user)); Assert.Equal(await manager.GetEmailAsync(user), oldEmail); Assert.Equal(stamp, user.SecurityStamp); @@ -1328,8 +1469,9 @@ namespace Microsoft.AspNet.Identity.Test Assert.Null(messageService.Message); IdentityResultAssert.IsSuccess(await manager.NotifyTwoFactorTokenAsync(user, factorId, token)); Assert.NotNull(messageService.Message); - Assert.Equal("Your security code is: "+token, messageService.Message.Body); + Assert.Equal("Your security code is: " + token, messageService.Message.Body); Assert.True(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "VerifyTwoFactorTokenAsync", user.Id.ToString()); } [Fact] @@ -1369,11 +1511,13 @@ namespace Microsoft.AspNet.Identity.Test Assert.NotNull(stamp); var token = await manager.GenerateTwoFactorTokenAsync(user, factorId); Assert.NotNull(token); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "GenerateTwoFactorTokenAsync", user.Id.ToString()); Assert.Null(messageService.Message); IdentityResultAssert.IsSuccess(await manager.NotifyTwoFactorTokenAsync(user, factorId, token)); Assert.NotNull(messageService.Message); Assert.Equal(subject, messageService.Message.Subject); Assert.Equal(string.Format(body, token), messageService.Message.Body); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "NotifyTwoFactorTokenAsync", user.Id.ToString()); Assert.True(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); } @@ -1403,6 +1547,7 @@ namespace Microsoft.AspNet.Identity.Test var stamp = user.SecurityStamp; Assert.NotNull(stamp); IdentityResultAssert.IsSuccess(await manager.SetTwoFactorEnabledAsync(user, true)); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "SetTwoFactorEnabledAsync", user.Id.ToString()); Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); Assert.True(await manager.GetTwoFactorEnabledAsync(user)); } @@ -1439,7 +1584,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Null(messageService.Message); IdentityResultAssert.IsSuccess(await manager.NotifyTwoFactorTokenAsync(user, factorId, token)); Assert.NotNull(messageService.Message); - Assert.Equal("Your security code is: "+token, messageService.Message.Body); + Assert.Equal("Your security code is: " + token, messageService.Message.Body); Assert.True(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); } @@ -1505,6 +1650,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.NotNull(factors); Assert.False(factors.Any()); IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "111-111-1111")); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "SetPhoneNumberAsync", user.Id.ToString()); user.PhoneNumberConfirmed = true; await manager.UpdateAsync(user); factors = await manager.GetValidTwoFactorProvidersAsync(user); @@ -1512,6 +1658,7 @@ namespace Microsoft.AspNet.Identity.Test Assert.Equal(1, factors.Count()); Assert.Equal("Phone", factors[0]); IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, "test@test.com")); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "SetEmailAsync", user.Id.ToString()); user.EmailConfirmed = true; await manager.UpdateAsync(user); factors = await manager.GetValidTwoFactorProvidersAsync(user); @@ -1560,6 +1707,7 @@ namespace Microsoft.AspNet.Identity.Test user.PhoneNumber = "4251234567"; IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); Assert.False(await manager.VerifyTwoFactorTokenAsync(user, "Phone", "bogus")); + IdentityResultAssert.VerifyUserManagerFailureLog(manager.Logger, "VerifyTwoFactorTokenAsync", user.Id.ToString(), IdentityErrorDescriber.Default.InvalidToken()); } [Fact] @@ -1581,6 +1729,60 @@ namespace Microsoft.AspNet.Identity.Test Assert.Equal(DateTimeOffset.Parse("01/01/2014"), await userMgr.GetLockoutEndDateAsync(user)); } + [Fact] + public async Task CanGetUsersWithClaims() + { + var manager = CreateManager(); + + for (int i = 0; i < 6; i++) + { + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + if ((i % 2) == 0) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("foo", "bar"))); + } + } + + Assert.Equal(3, (await manager.GetUsersForClaimAsync(new Claim("foo", "bar"))).Count); + + Assert.Equal(0, (await manager.GetUsersForClaimAsync(new Claim("123", "456"))).Count); + } + + [Fact] + public async Task CanGetUsersInRole() + { + var context = CreateTestContext(); + var manager = CreateManager(context); + var roleManager = CreateRoleManager(context); + var roles = GenerateRoles("UsersInRole", 4); + + foreach (var role in roles) + { + IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(role)); + } + + for (int i = 0; i < 6; i++) + { + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + if ((i % 2) == 0) + { + IdentityResultAssert.IsSuccess(await manager.AddToRolesAsync(user, roles.Select(x => x.Name).AsEnumerable())); + IdentityResultAssert.VerifyUserManagerSuccessLog(manager.Logger, "AddToRolesAsync", user.Id.ToString()); + } + } + + foreach (var role in roles) + { + Assert.Equal(3, (await manager.GetUsersInRoleAsync(role.Name)).Count); + } + + Assert.Equal(0, (await manager.GetUsersInRoleAsync("123456")).Count); + } + public List GenerateUsers(string userNamePrefix, int count) { var users = new List(count); @@ -1600,6 +1802,5 @@ namespace Microsoft.AspNet.Identity.Test } return roles; } - } }