Add NormalizedUserName / IUserNameNormalizer

+ Use normalized username for FindByUserName
This commit is contained in:
Hao Kung 2014-07-29 13:45:16 -07:00
parent 7942d2bc82
commit 626362d8a2
16 changed files with 235 additions and 36 deletions

View File

@ -114,6 +114,29 @@ namespace Microsoft.AspNet.Identity.EntityFramework
return Task.FromResult(0);
}
public Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = new 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 = new 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();
@ -183,16 +206,16 @@ namespace Microsoft.AspNet.Identity.EntityFramework
}
/// <summary>
/// Find a user by name
/// Find a user by normalized name
/// </summary>
/// <param name="userName"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual Task<TUser> FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken))
public virtual Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return GetUserAggregate(u => u.UserName.ToUpper() == userName.ToUpper(), cancellationToken);
return GetUserAggregate(u => u.NormalizedUserName == normalizedUserName, cancellationToken);
}
public IQueryable<TUser> Users

View File

@ -0,0 +1,18 @@
// 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
{
/// <summary>
/// Used to normalize a user name
/// </summary>
public interface IUserNameNormalizer
{
/// <summary>
/// Returns the normalized user name
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
string Normalize(string userName);
}
}

View File

@ -39,6 +39,24 @@ namespace Microsoft.AspNet.Identity
Task SetUserNameAsync(TUser user, string userName,
CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Returns the normalized user name
/// </summary>
/// <param name="user"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Set the normalized user name
/// </summary>
/// <param name="user"></param>
/// <param name="userName"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task SetNormalizedUserNameAsync(TUser user, string userName,
CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Insert a new user
/// </summary>
@ -72,11 +90,11 @@ namespace Microsoft.AspNet.Identity
Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Returns the user associated with this name
/// Returns the user associated with this normalized user name
/// </summary>
/// <param name="name"></param>
/// <param name="normalizedUserName"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<TUser> FindByNameAsync(string name, CancellationToken cancellationToken = default(CancellationToken));
Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken));
}
}

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Identity
yield return describe.Transient<IUserValidator<TUser>, UserValidator<TUser>>();
yield return describe.Transient<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
yield return describe.Transient<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
yield return describe.Transient<IUserNameNormalizer, UpperInvariantUserNameNormalizer>();
// TODO: rationalize email/sms/usertoken services
}

View File

@ -36,6 +36,7 @@ namespace Microsoft.AspNet.Identity
public virtual TKey Id { get; set; }
public virtual string UserName { get; set; }
public virtual string NormalizedUserName { get; set; }
/// <summary>
/// Email

View File

@ -25,6 +25,8 @@
<Compile Include="ClaimsIdentityFactory.cs" />
<Compile Include="ClaimsIdentityOptions.cs" />
<Compile Include="Crypto.cs" />
<Compile Include="UpperInvariantUserNameNormalizer.cs" />
<Compile Include="IUserNameNormalizer.cs" />
<Compile Include="PhoneNumberTokenProvider.cs" />
<Compile Include="IAuthenticationManager.cs" />
<Compile Include="IClaimsIdentityFactory.cs" />

View File

@ -0,0 +1,27 @@
// 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
{
/// <summary>
/// Normalizes user names via ToUpperInvariant()
/// </summary>
public class UpperInvariantUserNameNormalizer : IUserNameNormalizer
{
/// <summary>
/// Normalizes user names via ToUpperInvariant()
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public string Normalize(string userName)
{
if (userName == null)
{
return null;
}
return userName.Normalize().ToUpperInvariant();
}
}
}

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNet.Identity
/// <param name="claimsIdentityFactory"></param>
public UserManager(IUserStore<TUser> store, IOptionsAccessor<IdentityOptions> optionsAccessor,
IPasswordHasher<TUser> passwordHasher, IUserValidator<TUser> userValidator,
IPasswordValidator<TUser> passwordValidator)
IPasswordValidator<TUser> passwordValidator, IUserNameNormalizer userNameNormalizer)
{
if (store == null)
{
@ -57,6 +57,7 @@ namespace Microsoft.AspNet.Identity
PasswordHasher = passwordHasher;
UserValidator = userValidator;
PasswordValidator = passwordValidator;
UserNameNormalizer = userNameNormalizer;
// TODO: Email/Sms/Token services
}
@ -96,6 +97,11 @@ namespace Microsoft.AspNet.Identity
/// </summary>
public IPasswordValidator<TUser> PasswordValidator { get; set; }
/// <summary>
/// Used to normalize user names for uniqueness
/// </summary>
public IUserNameNormalizer UserNameNormalizer { get; set; }
/// <summary>
/// Used to send email
/// </summary>
@ -309,6 +315,7 @@ namespace Microsoft.AspNet.Identity
{
await GetUserLockoutStore().SetLockoutEnabledAsync(user, true, cancellationToken);
}
await UpdateNormalizedUserName(user, cancellationToken);
await Store.CreateAsync(user, cancellationToken);
return IdentityResult.Success;
}
@ -332,6 +339,7 @@ namespace Microsoft.AspNet.Identity
{
return result;
}
await UpdateNormalizedUserName(user, cancellationToken);
await Store.UpdateAsync(user, cancellationToken);
return IdentityResult.Success;
}
@ -381,6 +389,7 @@ namespace Microsoft.AspNet.Identity
{
throw new ArgumentNullException("userName");
}
userName = NormalizeUserName(userName);
return Store.FindByNameAsync(userName, cancellationToken);
}
@ -423,6 +432,29 @@ namespace Microsoft.AspNet.Identity
return await CreateAsync(user, cancellationToken);
}
/// <summary>
/// Normalize a user name for uniqueness comparisons
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public virtual string NormalizeUserName(string userName)
{
return (UserNameNormalizer == null) ? userName : UserNameNormalizer.Normalize(userName);
}
/// <summary>
/// Update the user's normalized user name
/// </summary>
/// <param name="user"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual async Task UpdateNormalizedUserName(TUser user,
CancellationToken cancellationToken = default(CancellationToken))
{
string userName = await GetUserNameAsync(user, cancellationToken);
await Store.SetNormalizedUserNameAsync(user, NormalizeUserName(userName), cancellationToken);
}
/// <summary>
/// Get the user's name
/// </summary>
@ -456,6 +488,7 @@ namespace Microsoft.AspNet.Identity
throw new ArgumentNullException("user");
}
await Store.SetUserNameAsync(user, userName, cancellationToken);
await UpdateNormalizedUserName(user, cancellationToken);
return await UpdateAsync(user, cancellationToken);
}
@ -483,6 +516,7 @@ namespace Microsoft.AspNet.Identity
CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
userName = NormalizeUserName(userName);
var user = await FindByNameAsync(userName, cancellationToken);
if (user == null)
{

View File

@ -15,6 +15,7 @@
"System.Diagnostics.Debug": "4.0.10.0",
"System.Diagnostics.Tools": "4.0.0.0",
"System.Globalization": "4.0.10.0",
"System.Globalization.Extensions": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Linq.Expressions": "4.0.0.0",
"System.Linq.Queryable": "4.0.0.0",

View File

@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test
public virtual TKey Id { get; set; }
public virtual string UserName { get; set; }
public virtual string NormalizedUserName { get; set; }
/// <summary>
/// Email

View File

@ -117,6 +117,29 @@ namespace Microsoft.AspNet.Identity.EntityFramework.InMemory.Test
return Task.FromResult(0);
}
public Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = new 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 = new 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();

View File

@ -313,6 +313,17 @@ namespace Microsoft.AspNet.Identity.InMemory
return Task.FromResult(user.TwoFactorEnabled);
}
public Task<string> GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(user.NormalizedUserName);
}
public Task SetNormalizedUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken))
{
user.NormalizedUserName = userName;
return Task.FromResult(0);
}
private class LoginComparer : IEqualityComparer<UserLoginInfo>
{
public bool Equals(UserLoginInfo x, UserLoginInfo y)

View File

@ -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 System.Threading;
using System.Threading.Tasks;
@ -51,5 +52,15 @@ namespace Microsoft.AspNet.Identity.Test
{
return Task.FromResult(0);
}
public Task<string> GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult<string>(null);
}
public Task SetNormalizedUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(0);
}
}
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Identity.Test
public TestManager(IUserStore<TestUser> store, IOptionsAccessor<IdentityOptions> optionsAccessor,
IPasswordHasher<TestUser> passwordHasher, IUserValidator<TestUser> userValidator,
IPasswordValidator<TestUser> passwordValidator)
: base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator) { }
: base(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null) { }
}
[Fact]
@ -130,13 +130,31 @@ namespace Microsoft.AspNet.Identity.Test
}
[Fact]
public async Task FindByNameCallsStore()
public async Task FindByNameCallsStoreWithNormalizedName()
{
// Setup
var store = new Mock<IUserStore<TestUser>>();
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<TestUser>(store.Object);
// Act
var result = await userManager.FindByNameAsync(user.UserName);
// Assert
Assert.Equal(user, result);
store.VerifyAll();
}
[Fact]
public async Task CanFindByNameCallsStoreWithoutNormalizedName()
{
// Setup
var store = new Mock<IUserStore<TestUser>>();
var user = new TestUser {UserName="Foo"};
store.Setup(s => s.FindByNameAsync(user.UserName, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable();
var userManager = MockHelpers.TestUserManager<TestUser>(store.Object);
userManager.UserNameNormalizer = null;
// Act
var result = await userManager.FindByNameAsync(user.UserName);
@ -333,14 +351,10 @@ namespace Microsoft.AspNet.Identity.Test
Assert.False(manager.SupportsUserSecurityStamp);
await Assert.ThrowsAsync<NotSupportedException>(() => manager.UpdateSecurityStampAsync(null));
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetSecurityStampAsync(null));
#if NET45
await
Assert.ThrowsAsync<NotSupportedException>(
await Assert.ThrowsAsync<NotSupportedException>(
() => manager.VerifyChangePhoneNumberTokenAsync(null, "1", "111-111-1111"));
await
Assert.ThrowsAsync<NotSupportedException>(
await Assert.ThrowsAsync<NotSupportedException>(
() => manager.GenerateChangePhoneNumberTokenAsync(null, "111-111-1111"));
#endif
}
[Fact]
@ -428,13 +442,13 @@ namespace Microsoft.AspNet.Identity.Test
var passwordValidator = new PasswordValidator<TestUser>();
Assert.Throws<ArgumentNullException>("store",
() => new UserManager<TestUser>(null, null, null, null, null));
() => new UserManager<TestUser>(null, null, null, null, null, null));
Assert.Throws<ArgumentNullException>("optionsAccessor",
() => new UserManager<TestUser>(store, null, null, null, null));
() => new UserManager<TestUser>(store, null, null, null, null, null));
Assert.Throws<ArgumentNullException>("passwordHasher",
() => new UserManager<TestUser>(store, optionsAccessor, null, null, null));
() => new UserManager<TestUser>(store, optionsAccessor, null, null, null, null));
var manager = new UserManager<TestUser>(store, optionsAccessor, passwordHasher, userValidator, passwordValidator);
var manager = new UserManager<TestUser>(store, optionsAccessor, passwordHasher, userValidator, passwordValidator, null);
Assert.Throws<ArgumentNullException>("value", () => manager.PasswordHasher = null);
Assert.Throws<ArgumentNullException>("value", () => manager.Options = null);
@ -836,6 +850,16 @@ namespace Microsoft.AspNet.Identity.Test
{
return Task.FromResult<string>(null);
}
public Task<string> GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult<string>(null);
}
public Task SetNormalizedUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.FromResult(0);
}
}
private class NoOpTokenProvider : IUserTokenProvider<TestUser>
@ -1076,6 +1100,16 @@ namespace Microsoft.AspNet.Identity.Test
{
throw new NotImplementedException();
}
public Task<string> GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
public Task SetNormalizedUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
}
}

View File

@ -6,20 +6,6 @@ using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Identity.Test
{
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store, IOptionsAccessor<IdentityOptions> options,
IPasswordHasher<ApplicationUser> passwordHasher, IUserValidator<ApplicationUser> userValidator,
IPasswordValidator<ApplicationUser> passwordValidator)
: base(store, options, passwordHasher, userValidator, passwordValidator) { }
}
public class ApplicationRoleManager : RoleManager<IdentityRole>
{
public ApplicationRoleManager(IRoleStore<IdentityRole> store, IRoleValidator<IdentityRole> roleValidator)
: base(store, roleValidator) { }
}
public class ApplicationUser : IdentityUser
{
}

View File

@ -33,7 +33,13 @@ namespace Microsoft.AspNet.Identity.Test
{
var store = new Mock<IUserStore<TUser>>();
var options = new OptionsAccessor<IdentityOptions>(null);
return new Mock<UserManager<TUser>>(store.Object, options, new PasswordHasher<TUser>(), new UserValidator<TUser>(), new PasswordValidator<TUser>());
return new Mock<UserManager<TUser>>(
store.Object,
options,
new PasswordHasher<TUser>(),
new UserValidator<TUser>(),
new PasswordValidator<TUser>(),
new UpperInvariantUserNameNormalizer());
}
public static Mock<RoleManager<TRole>> MockRoleManager<TRole>() where TRole : class
@ -51,8 +57,10 @@ namespace Microsoft.AspNet.Identity.Test
{
var options = new OptionsAccessor<IdentityOptions>(null);
var validator = new Mock<UserValidator<TUser>>();
var userManager = new UserManager<TUser>(store, options, new PasswordHasher<TUser>(), validator.Object, new PasswordValidator<TUser>());
validator.Setup(v => v.ValidateAsync(userManager, It.IsAny<TUser>(), CancellationToken.None)).Returns(Task.FromResult(IdentityResult.Success)).Verifiable();
var userManager = new UserManager<TUser>(store, options, new PasswordHasher<TUser>(),
validator.Object, new PasswordValidator<TUser>(), new UpperInvariantUserNameNormalizer());
validator.Setup(v => v.ValidateAsync(userManager, It.IsAny<TUser>(), CancellationToken.None))
.Returns(Task.FromResult(IdentityResult.Success)).Verifiable();
return userManager;
}
}