diff --git a/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs b/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs new file mode 100644 index 0000000000..1b9fa3e8d8 --- /dev/null +++ b/src/Microsoft.AspNet.Identity/ClaimsIdentityFactory.cs @@ -0,0 +1,107 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Identity +{ + /// + /// Creates a ClaimsIdentity from a User + /// + /// + /// + public class ClaimsIdentityFactory : IClaimsIdentityFactory + where TUser : class, IUser + where TKey : IEquatable + { + /// + /// ClaimType used for the security stamp by default + /// + public const string DefaultSecurityStampClaimType = "AspNet.Identity.SecurityStamp"; + + /// + /// Constructor + /// + public ClaimsIdentityFactory() + { + RoleClaimType = ClaimsIdentity.DefaultRoleClaimType; + UserIdClaimType = ClaimTypes.NameIdentifier; + UserNameClaimType = ClaimsIdentity.DefaultNameClaimType; + SecurityStampClaimType = DefaultSecurityStampClaimType; + } + + /// + /// Claim type used for role claims + /// + public string RoleClaimType { get; set; } + + /// + /// Claim type used for the user name + /// + public string UserNameClaimType { get; set; } + + /// + /// Claim type used for the user id + /// + public string UserIdClaimType { get; set; } + + /// + /// Claim type used for the user security stamp + /// + public string SecurityStampClaimType { get; set; } + + /// + /// Create a ClaimsIdentity from a user + /// + /// + /// + /// + /// + public virtual async Task Create(UserManager manager, TUser user, + string authenticationType) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + if (user == null) + { + throw new ArgumentNullException("user"); + } + var id = new ClaimsIdentity(authenticationType, UserNameClaimType, RoleClaimType); + id.AddClaim(new Claim(UserIdClaimType, ConvertIdToString(user.Id), ClaimValueTypes.String)); + id.AddClaim(new Claim(UserNameClaimType, user.UserName, ClaimValueTypes.String)); + if (manager.SupportsUserSecurityStamp) + { + id.AddClaim(new Claim(SecurityStampClaimType, + await manager.GetSecurityStamp(user.Id).ConfigureAwait(false))); + } + if (manager.SupportsUserRole) + { + var roles = await manager.GetRoles(user.Id).ConfigureAwait(false); + foreach (var roleName in roles) + { + id.AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String)); + } + } + if (manager.SupportsUserClaim) + { + id.AddClaims(await manager.GetClaims(user.Id).ConfigureAwait(false)); + } + return id; + } + + /// + /// Convert the key to a string, by default just calls .ToString() + /// + /// + /// + public virtual string ConvertIdToString(TKey key) + { + if (key == null || key.Equals(default(TKey))) + { + return null; + } + return key.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Identity/UserManager.cs b/src/Microsoft.AspNet.Identity/UserManager.cs index 1d77de8716..40cedb7985 100644 --- a/src/Microsoft.AspNet.Identity/UserManager.cs +++ b/src/Microsoft.AspNet.Identity/UserManager.cs @@ -39,8 +39,8 @@ namespace Microsoft.AspNet.Identity PasswordHasher = serviceProvider.GetService(); UserValidator = serviceProvider.GetService>(); PasswordValidator = serviceProvider.GetService(); + ClaimsIdentityFactory = serviceProvider.GetService>(); //TODO: Store = serviceProvider.GetService>(); - //TODO: ClaimsIdentityFactory = serviceProvider.GetService>(); // TODO: maybe each optional store as well? Email and SMS services? } @@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Identity Store = store; UserValidator = new UserValidator(); PasswordHasher = new PasswordHasher(); - //TODO: ClaimsIdentityFactory = new ClaimsIdentityFactory(); + ClaimsIdentityFactory = new ClaimsIdentityFactory(); } /// diff --git a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs index 91dffbc9ef..ee44d8ec47 100644 --- a/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs +++ b/test/Microsoft.AspNet.Identity.InMemory.Test/InMemoryStoreTest.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test } [Fact] - public async Task ValidatorCanBlockCreate() + public async Task UserValidatorCanBlockCreate() { var manager = CreateManager(); var user = new InMemoryUser("CreateBlocked"); @@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test } [Fact] - public async Task ValidatorCanBlockUpdate() + public async Task UserValidatorCanBlockUpdate() { var manager = CreateManager(); var user = new InMemoryUser("UpdateBlocked"); @@ -51,6 +51,50 @@ namespace Microsoft.AspNet.Identity.InMemory.Test IdentityResultAssert.IsFailure(await manager.Update(user), AlwaysBadValidator.ErrorMessage); } + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task UserValidatorBlocksShortEmailsWhenRequiresUniqueEmail(string email) + { + var manager = CreateManager(); + var user = new InMemoryUser("UpdateBlocked") { Email = email }; + manager.UserValidator = new UserValidator { RequireUniqueEmail = true }; + IdentityResultAssert.IsFailure(await manager.Create(user), "Email cannot be null or empty."); + } + +#if NET45 + [Theory] + [InlineData("@@afd")] + [InlineData("bogus")] + public async Task UserValidatorBlocksInvalidEmailsWhenRequiresUniqueEmail(string email) + { + var manager = CreateManager(); + var user = new InMemoryUser("UpdateBlocked") { Email = email }; + manager.UserValidator = new UserValidator { RequireUniqueEmail = true }; + IdentityResultAssert.IsFailure(await manager.Create(user), "Email '"+email+"' is invalid."); + } +#endif + + [Fact] + public async Task PasswordValidatorCanBlockAddPassword() + { + var manager = CreateManager(); + var user = new InMemoryUser("AddPasswordBlocked"); + IdentityResultAssert.IsSuccess(await manager.Create(user)); + manager.PasswordValidator = new AlwaysBadValidator(); + IdentityResultAssert.IsFailure(await manager.AddPassword(user.Id, "password"), AlwaysBadValidator.ErrorMessage); + } + + [Fact] + public async Task PasswordValidatorCanBlockChangePassword() + { + var manager = CreateManager(); + var user = new InMemoryUser("ChangePasswordBlocked"); + IdentityResultAssert.IsSuccess(await manager.Create(user, "password")); + manager.PasswordValidator = new AlwaysBadValidator(); + IdentityResultAssert.IsFailure(await manager.ChangePassword(user.Id, "password", "new"), AlwaysBadValidator.ErrorMessage); + } + [Fact] public async Task CanCreateUserNoPassword() { @@ -291,6 +335,43 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.Null(usersQ.FirstOrDefault(u => u.UserName == "bogus")); } + [Fact] + public async Task ClaimsIdentityCreatesExpectedClaims() + { + var manager = CreateManager(); + var role = CreateRoleManager(); + var user = new InMemoryUser("Hao"); + IdentityResultAssert.IsSuccess(await manager.Create(user)); + IdentityResultAssert.IsSuccess(await role.Create(new InMemoryRole("Admin"))); + IdentityResultAssert.IsSuccess(await role.Create(new InMemoryRole("Local"))); + IdentityResultAssert.IsSuccess(await manager.AddToRole(user.Id, "Admin")); + IdentityResultAssert.IsSuccess(await manager.AddToRole(user.Id, "Local")); + Claim[] userClaims = + { + new Claim("Whatever", "Value"), + new Claim("Whatever2", "Value2") + }; + foreach (var c in userClaims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaim(user.Id, c)); + } + + var identity = await manager.CreateIdentity(user, "test"); + var claimsFactory = manager.ClaimsIdentityFactory as ClaimsIdentityFactory; + Assert.NotNull(claimsFactory); + var claims = identity.Claims; + Assert.NotNull(claims); + Assert.True( + claims.Any(c => c.Type == claimsFactory.UserNameClaimType && c.Value == user.UserName)); + Assert.True(claims.Any(c => c.Type == claimsFactory.UserIdClaimType && c.Value == user.Id)); + Assert.True(claims.Any(c => c.Type == claimsFactory.RoleClaimType && c.Value == "Admin")); + Assert.True(claims.Any(c => c.Type == claimsFactory.RoleClaimType && c.Value == "Local")); + foreach (var cl in userClaims) + { + Assert.True(claims.Any(c => c.Type == cl.Type && c.Value == cl.Value)); + } + } + [Fact] public async Task ConfirmEmailFalseByDefaultTest() { @@ -331,7 +412,6 @@ namespace Microsoft.AspNet.Identity.InMemory.Test } } - [Fact] public async Task CanResetPasswordWithStaticTokenProvider() { @@ -351,6 +431,43 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.NotEqual(stamp, user.SecurityStamp); } + [Fact] + public async Task PasswordValidatorCanBlockResetPasswordWithStaticTokenProvider() + { + var manager = CreateManager(); + manager.UserTokenProvider = new StaticTokenProvider(); + var user = new InMemoryUser("ResetPasswordTest"); + const string password = "password"; + const string newPassword = "newpassword"; + IdentityResultAssert.IsSuccess(await manager.Create(user, password)); + var stamp = user.SecurityStamp; + Assert.NotNull(stamp); + var token = await manager.GeneratePasswordResetToken(user.Id); + Assert.NotNull(token); + manager.PasswordValidator = new AlwaysBadValidator(); + IdentityResultAssert.IsFailure(await manager.ResetPassword(user.Id, token, newPassword), AlwaysBadValidator.ErrorMessage); + Assert.NotNull(await manager.Find(user.UserName, password)); + Assert.Equal(user, await manager.Find(user.UserName, password)); + Assert.Equal(stamp, user.SecurityStamp); + } + + [Fact] + public async Task ResetPasswordWithStaticTokenProviderFailsWithWrongToken() + { + var manager = CreateManager(); + manager.UserTokenProvider = new StaticTokenProvider(); + var user = new InMemoryUser("ResetPasswordTest"); + const string password = "password"; + const string newPassword = "newpassword"; + IdentityResultAssert.IsSuccess(await manager.Create(user, password)); + var stamp = user.SecurityStamp; + Assert.NotNull(stamp); + IdentityResultAssert.IsFailure(await manager.ResetPassword(user.Id, "bogus", newPassword), "Invalid token."); + Assert.NotNull(await manager.Find(user.UserName, password)); + Assert.Equal(user, await manager.Find(user.UserName, password)); + Assert.Equal(stamp, user.SecurityStamp); + } + [Fact] public async Task CanGenerateAndVerifyUserTokenWithStaticTokenProvider() { @@ -592,7 +709,7 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.True(await manager.RoleExists(role.Name)); } - private class AlwaysBadValidator : IUserValidator, IRoleValidator + private class AlwaysBadValidator : IUserValidator, IRoleValidator, IPasswordValidator { public const string ErrorMessage = "I'm Bad."; @@ -605,6 +722,11 @@ namespace Microsoft.AspNet.Identity.InMemory.Test { return Task.FromResult(IdentityResult.Failed(ErrorMessage)); } + + public Task Validate(string password) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } } [Fact] @@ -1041,6 +1163,16 @@ namespace Microsoft.AspNet.Identity.InMemory.Test Assert.True(await manager.VerifyTwoFactorToken(user.Id, factorId, token)); } + [Fact] + public async Task NotifyWithUnknownProviderFails() + { + var manager = CreateManager(); + var user = new InMemoryUser("NotifyFail"); + IdentityResultAssert.IsSuccess(await manager.Create(user)); + await ExceptionAssert.ThrowsAsync(async () => await manager.NotifyTwoFactorToken(user.Id, "Bogus", "token"), "No IUserTwoFactorProvider for 'Bogus' is registered."); + } + + //[Fact] //public async Task EmailTokenFactorWithFormatTest() //{ diff --git a/test/Microsoft.AspNet.Identity.Test/ClaimsIdentityFactoryTest.cs b/test/Microsoft.AspNet.Identity.Test/ClaimsIdentityFactoryTest.cs new file mode 100644 index 0000000000..42f4fc2464 --- /dev/null +++ b/test/Microsoft.AspNet.Identity.Test/ClaimsIdentityFactoryTest.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Testing; +using Xunit; + +namespace Microsoft.AspNet.Identity.Test +{ + public class ClaimsIdentityFactoryTest + { + [Fact] + public async Task CreateIdentityNullChecks() + { + var factory = new ClaimsIdentityFactory(); + var manager = new UserManager(new NoopUserStore()); + await Assert.ThrowsAsync("manager", + async () => await factory.Create(null, null, "whatever")); + await Assert.ThrowsAsync("user", + async () => await factory.Create(manager, null, "whatever")); + await Assert.ThrowsAsync("value", + async () => await factory.Create(manager, new TestUser(), null)); + } + + [Fact] + public void ConvertIdToStringWithDefaultStringReturnsNull() + { + var factory = new ClaimsIdentityFactory(); + Assert.Null(factory.ConvertIdToString(default(string))); + } + + [Fact] + public void ConvertIdToStringWithDefaultIntReturnsNull() + { + var factory = new ClaimsIdentityFactory, int>(); + Assert.Null(factory.ConvertIdToString(default(int))); + } + + [Fact] + public void ConvertIdToStringWithDefaultGuidReturnsNull() + { + var factory = new ClaimsIdentityFactory, Guid>(); + Assert.Null(factory.ConvertIdToString(default(Guid))); + } + + // TODO: Need Mock (test in InMemory for now) + //[Fact] + //public async Task ClaimsIdentityTest() + //{ + // var db = UnitTestHelper.CreateDefaultDb(); + // var manager = new UserManager(new UserStore(db)); + // var role = new RoleManager(new RoleStore(db)); + // var user = new TestUser("Hao"); + // UnitTestHelper.IsSuccess(await manager.CreateAsync(user)); + // UnitTestHelper.IsSuccess(await role.CreateAsync(new IdentityRole("Admin"))); + // UnitTestHelper.IsSuccess(await role.CreateAsync(new IdentityRole("Local"))); + // UnitTestHelper.IsSuccess(await manager.AddToRoleAsync(user.Id, "Admin")); + // UnitTestHelper.IsSuccess(await manager.AddToRoleAsync(user.Id, "Local")); + // Claim[] userClaims = + // { + // new Claim("Whatever", "Value"), + // new Claim("Whatever2", "Value2") + // }; + // foreach (var c in userClaims) + // { + // UnitTestHelper.IsSuccess(await manager.AddClaimAsync(user.Id, c)); + // } + + // var identity = await manager.CreateIdentityAsync(user, "test"); + // var claimsFactory = manager.ClaimsIdentityFactory as ClaimsIdentityFactory; + // Assert.NotNull(claimsFactory); + // var claims = identity.Claims; + // Assert.NotNull(claims); + // Assert.True( + // claims.Any(c => c.Type == claimsFactory.UserNameClaimType && c.Value == user.UserName)); + // Assert.True(claims.Any(c => c.Type == claimsFactory.UserIdClaimType && c.Value == user.Id)); + // Assert.True(claims.Any(c => c.Type == claimsFactory.RoleClaimType && c.Value == "Admin")); + // Assert.True(claims.Any(c => c.Type == claimsFactory.RoleClaimType && c.Value == "Local")); + // Assert.True( + // claims.Any( + // c => + // c.Type == ClaimsIdentityFactory.IdentityProviderClaimType && + // c.Value == ClaimsIdentityFactory.DefaultIdentityProviderClaimValue)); + // foreach (var cl in userClaims) + // { + // Assert.True(claims.Any(c => c.Type == cl.Type && c.Value == cl.Value)); + // } + //} + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/TestServices.cs b/test/Microsoft.AspNet.Identity.Test/TestServices.cs index 8ba4a45028..115a1ea98b 100644 --- a/test/Microsoft.AspNet.Identity.Test/TestServices.cs +++ b/test/Microsoft.AspNet.Identity.Test/TestServices.cs @@ -11,8 +11,7 @@ namespace Microsoft.AspNet.Identity.Test where TUser : class,IUser where TKey : IEquatable { - var serviceCollection = new ServiceCollection(); - serviceCollection.Add(TestServices.DefaultServices()); + var serviceCollection = new ServiceCollection { DefaultServices() }; return serviceCollection.BuildServiceProvider(); } @@ -25,6 +24,7 @@ namespace Microsoft.AspNet.Identity.Test new ServiceDescriptor(), new ServiceDescriptor, UserValidator>(), new ServiceDescriptor(), + new ServiceDescriptor, ClaimsIdentityFactory>(), }; } @@ -52,30 +52,5 @@ namespace Microsoft.AspNet.Identity.Test get { return null; } } } - - public class ServiceInstanceDescriptor : IServiceDescriptor - { - public ServiceInstanceDescriptor(object instance) - { - ImplementationInstance = instance; - } - - public LifecycleKind Lifecycle - { - get { return LifecycleKind.Singleton; } - } - - public Type ServiceType - { - get { return typeof(TService); } - } - - public Type ImplementationType - { - get { return null; } - } - - public object ImplementationInstance { get; private set; } - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Identity.Test/TestUser.cs b/test/Microsoft.AspNet.Identity.Test/TestUser.cs index c27bfabac8..cf8ba9bf9c 100644 --- a/test/Microsoft.AspNet.Identity.Test/TestUser.cs +++ b/test/Microsoft.AspNet.Identity.Test/TestUser.cs @@ -1,9 +1,14 @@ namespace Microsoft.AspNet.Identity.Test { - public class TestUser : IUser + public class TestUser : TestUser { - public string Id { get; private set; } + } + + public class TestUser : IUser + { + public TKey Id { get; private set; } public string UserName { get; set; } } + } diff --git a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs index 5b27b5aa3c..c26d977060 100644 --- a/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs +++ b/test/Microsoft.AspNet.Identity.Test/UserManagerTest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime; using System.Security.Claims; using Microsoft.AspNet.Testing; using Moq; @@ -38,6 +39,20 @@ namespace Microsoft.AspNet.Identity.Test // store.VerifyAll(); //} + [Fact] + public async Task CheckPasswordWithNullUserReturnsFalse() + { + var manager = new UserManager(new EmptyStore()); + Assert.False(await manager.CheckPassword(null, "whatevs")); + } + + [Fact] + public async Task FindWithUnknownUserAndPasswordReturnsNull() + { + var manager = new UserManager(new EmptyStore()); + Assert.Null(await manager.Find("bogus", "whatevs")); + } + [Fact] public void UsersQueryableFailWhenStoreNotImplemented() { @@ -310,6 +325,7 @@ namespace Microsoft.AspNet.Identity.Test { var manager = new UserManager(new NoopUserStore()); manager.Dispose(); + Assert.Throws(() => manager.ClaimsIdentityFactory); await Assert.ThrowsAsync(() => manager.AddClaim("bogus", null)); await Assert.ThrowsAsync(() => manager.AddLogin("bogus", null)); await Assert.ThrowsAsync(() => manager.AddPassword("bogus", null));