Browse Source

Merge pull request #4979 from abpframework/external-auth

Implemented: Authentication Extensibility system to check username & password from an external source
pull/4993/head
maliming 6 years ago
committed by GitHub
parent
commit
02741a98d4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 99
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  2. 5
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs
  3. 9
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreOptions.cs
  4. 66
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs
  5. 156
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderBase.cs
  6. 18
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderDictionary.cs
  7. 25
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderInfo.cs
  8. 36
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginUserInfo.cs
  9. 32
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs
  10. 5
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs
  11. 42
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs
  12. 22
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs
  13. 3
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs
  14. 17
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestBase.cs
  15. 8
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs
  16. 24
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs
  17. 57
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ExternalLoginProvider_Tests.cs
  18. 55
      modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeExternalLoginProvider.cs
  19. 2
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityClaimTypeManager_Tests.cs
  20. 8
      templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.Designer.cs
  21. 1
      templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.cs
  22. 6
      templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/MyProjectNameMigrationsDbContextModelSnapshot.cs

99
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -78,11 +78,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
{ {
await CheckSelfRegistrationAsync(); await CheckSelfRegistrationAsync();
var registerDto = new RegisterDto()
{
AppName = "MVC"
};
if (IsExternalLogin) if (IsExternalLogin)
{ {
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
@ -92,90 +87,56 @@ namespace Volo.Abp.Account.Web.Pages.Account
return RedirectToPage("./Login"); return RedirectToPage("./Login");
} }
registerDto.EmailAddress = Input.EmailAddress; await RegisterExternalUserAsync(externalLoginInfo, Input.EmailAddress);
registerDto.UserName = Input.EmailAddress;
registerDto.Password = GeneratePassword();
} }
else else
{ {
ValidateModel(); await RegisterLocalUserAsync();
registerDto.EmailAddress = Input.EmailAddress;
registerDto.Password = Input.Password;
registerDto.UserName = Input.UserName;
}
var userDto = await AccountAppService.RegisterAsync(registerDto);
var user = await UserManager.GetByIdAsync(userDto.Id);
await SignInManager.SignInAsync(user, isPersistent: false);
if (IsExternalLogin)
{
await AddToUserLogins(user);
} }
return Redirect(ReturnUrl ?? "~/"); //TODO: How to ensure safety? IdentityServer requires it however it should be checked somehow! return Redirect(ReturnUrl ?? "~/"); //TODO: How to ensure safety? IdentityServer requires it however it should be checked somehow!
} }
protected virtual async Task AddToUserLogins(IdentityUser user) protected virtual async Task RegisterLocalUserAsync()
{ {
var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); ValidateModel();
var userLoginAlreadyExists = user.Logins.Any(x => var userDto = await AccountAppService.RegisterAsync(
x.TenantId == user.TenantId && new RegisterDto
x.LoginProvider == externalLoginInfo.LoginProvider && {
x.ProviderKey == externalLoginInfo.ProviderKey); AppName = "MVC",
EmailAddress = Input.EmailAddress,
Password = Input.Password,
UserName = Input.UserName
}
);
if (!userLoginAlreadyExists) var user = await UserManager.GetByIdAsync(userDto.Id);
{ await SignInManager.SignInAsync(user, isPersistent: true);
user.AddLogin(new UserLoginInfo(
externalLoginInfo.LoginProvider,
externalLoginInfo.ProviderKey,
externalLoginInfo.ProviderDisplayName
)
);
}
} }
protected virtual string GeneratePassword() protected virtual async Task RegisterExternalUserAsync(ExternalLoginInfo externalLoginInfo, string emailAddress)
{ {
var random = new Random(); var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
var options = UserManager.Options.Password;
int length = random.Next(options.RequiredLength, IdentityUserConsts.MaxPasswordLength - 1);
bool nonAlphanumeric = options.RequireNonAlphanumeric; (await UserManager.CreateAsync(user)).CheckErrors();
bool digit = options.RequireDigit; (await UserManager.AddDefaultRolesAsync(user)).CheckErrors();
bool lowercase = options.RequireLowercase;
bool uppercase = options.RequireUppercase;
StringBuilder password = new StringBuilder(); var userLoginAlreadyExists = user.Logins.Any(x =>
x.TenantId == user.TenantId &&
x.LoginProvider == externalLoginInfo.LoginProvider &&
x.ProviderKey == externalLoginInfo.ProviderKey);
while (password.Length < length) if (!userLoginAlreadyExists)
{ {
char c = (char)random.Next(32, 126); (await UserManager.AddLoginAsync(user, new UserLoginInfo(
externalLoginInfo.LoginProvider,
password.Append(c); externalLoginInfo.ProviderKey,
externalLoginInfo.ProviderDisplayName
if (char.IsDigit(c)) ))).CheckErrors();
digit = false;
else if (char.IsLower(c))
lowercase = false;
else if (char.IsUpper(c))
uppercase = false;
else if (!char.IsLetterOrDigit(c))
nonAlphanumeric = false;
} }
if (nonAlphanumeric) await SignInManager.SignInAsync(user, isPersistent: true);
password.Append((char)random.Next(33, 48));
if (digit)
password.Append((char)random.Next(48, 58));
if (lowercase)
password.Append((char)random.Next(97, 123));
if (uppercase)
password.Append((char)random.Next(65, 91));
return password.ToString();
} }
protected virtual async Task CheckSelfRegistrationAsync() protected virtual async Task CheckSelfRegistrationAsync()

5
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs

@ -13,8 +13,9 @@ namespace Volo.Abp.Identity.AspNetCore
{ {
PreConfigure<IdentityBuilder>(builder => PreConfigure<IdentityBuilder>(builder =>
{ {
builder.AddDefaultTokenProviders(); builder
builder.AddSignInManager(); .AddDefaultTokenProviders()
.AddSignInManager<AbpSignInManager>();
}); });
} }

9
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreOptions.cs

@ -6,5 +6,12 @@
/// Default: true. /// Default: true.
/// </summary> /// </summary>
public bool ConfigureAuthentication { get; set; } = true; public bool ConfigureAuthentication { get; set; } = true;
public ExternalLoginProviderDictionary ExternalLoginProviders { get; }
public AbpIdentityAspNetCoreOptions()
{
ExternalLoginProviders = new ExternalLoginProviderDictionary();
}
} }
} }

66
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs

@ -0,0 +1,66 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Volo.Abp.Identity.AspNetCore
{
public class AbpSignInManager : SignInManager<IdentityUser>
{
protected AbpIdentityAspNetCoreOptions AbpOptions { get; }
public AbpSignInManager(
IdentityUserManager userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<IdentityUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<IdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<IdentityUser> confirmation,
IOptions<AbpIdentityAspNetCoreOptions> options
) : base(
userManager,
contextAccessor,
claimsFactory,
optionsAccessor,
logger,
schemes,
confirmation)
{
AbpOptions = options.Value;
}
public override async Task<SignInResult> PasswordSignInAsync(
string userName,
string password,
bool isPersistent,
bool lockoutOnFailure)
{
foreach (var externalLoginProviderInfo in AbpOptions.ExternalLoginProviders.Values)
{
var externalLoginProvider = (IExternalLoginProvider) Context.RequestServices
.GetRequiredService(externalLoginProviderInfo.Type);
if (await externalLoginProvider.TryAuthenticateAsync(userName, password))
{
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
user = await externalLoginProvider.CreateUserAsync(userName, externalLoginProviderInfo.Name);
}
else
{
await externalLoginProvider.UpdateUserAsync(user, externalLoginProviderInfo.Name);
}
return await SignInOrTwoFactorAsync(user, isPersistent);
}
}
return await base.PasswordSignInAsync(userName, password, isPersistent, lockoutOnFailure);
}
}
}

156
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderBase.cs

@ -0,0 +1,156 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Identity.AspNetCore
{
public abstract class ExternalLoginProviderBase : IExternalLoginProvider
{
protected IGuidGenerator GuidGenerator { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IdentityUserManager UserManager { get; }
protected IIdentityUserRepository IdentityUserRepository { get; }
protected ExternalLoginProviderBase(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IdentityUserManager userManager,
IIdentityUserRepository identityUserRepository)
{
GuidGenerator = guidGenerator;
CurrentTenant = currentTenant;
UserManager = userManager;
IdentityUserRepository = identityUserRepository;
}
public abstract Task<bool> TryAuthenticateAsync(string userName, string plainPassword);
public virtual async Task<IdentityUser> CreateUserAsync(string userName, string providerName)
{
var externalUser = await GetUserInfoAsync(userName);
NormalizeExternalLoginUserInfo(externalUser, userName);
var user = new IdentityUser(
GuidGenerator.Create(),
userName,
externalUser.Email,
tenantId: CurrentTenant.Id
);
user.Name = externalUser.Name;
user.Surname = externalUser.Surname;
user.IsExternal = true;
user.SetEmailConfirmed(externalUser.EmailConfirmed ?? false);
user.SetPhoneNumber(externalUser.PhoneNumber, externalUser.PhoneNumberConfirmed ?? false);
(await UserManager.CreateAsync(user)).CheckErrors();
if (externalUser.TwoFactorEnabled != null)
{
(await UserManager.SetTwoFactorEnabledAsync(user, externalUser.TwoFactorEnabled.Value)).CheckErrors();
}
(await UserManager.AddDefaultRolesAsync(user)).CheckErrors();
(await UserManager.AddLoginAsync(
user,
new UserLoginInfo(
providerName,
externalUser.ProviderKey ,
providerName
)
)
).CheckErrors();
return user;
}
public virtual async Task UpdateUserAsync(IdentityUser user, string providerName)
{
var externalUser = await GetUserInfoAsync(user);
NormalizeExternalLoginUserInfo(externalUser, user.UserName);
if (!externalUser.Name.IsNullOrWhiteSpace())
{
user.Name = externalUser.Name;
}
if (!externalUser.Surname.IsNullOrWhiteSpace())
{
user.Surname = externalUser.Surname;
}
if (user.PhoneNumber != externalUser.PhoneNumber)
{
if (!externalUser.PhoneNumber.IsNullOrWhiteSpace())
{
await UserManager.SetPhoneNumberAsync(user, externalUser.PhoneNumber);
user.SetPhoneNumberConfirmed(externalUser.PhoneNumberConfirmed == true);
}
}
else
{
if (!user.PhoneNumber.IsNullOrWhiteSpace() &&
user.PhoneNumberConfirmed == false &&
externalUser.PhoneNumberConfirmed == true)
{
user.SetPhoneNumberConfirmed(true);
}
}
if (!string.Equals(user.Email, externalUser.Email, StringComparison.OrdinalIgnoreCase))
{
(await UserManager.SetEmailAsync(user, externalUser.Email)).CheckErrors();
user.SetEmailConfirmed(externalUser.EmailConfirmed ?? false);
}
if (externalUser.TwoFactorEnabled != null)
{
(await UserManager.SetTwoFactorEnabledAsync(user, externalUser.TwoFactorEnabled.Value)).CheckErrors();
}
await IdentityUserRepository.EnsureCollectionLoadedAsync(user, u => u.Logins);
var userLogin = user.Logins.FirstOrDefault(l => l.LoginProvider == providerName);
if (userLogin != null)
{
if (userLogin.ProviderKey != externalUser.ProviderKey)
{
(await UserManager.RemoveLoginAsync(user, providerName, userLogin.ProviderKey)).CheckErrors();
(await UserManager.AddLoginAsync(user, new UserLoginInfo(providerName, externalUser.ProviderKey, providerName))).CheckErrors();
}
}
else
{
(await UserManager.AddLoginAsync(user, new UserLoginInfo(providerName, externalUser.ProviderKey, providerName))).CheckErrors();
}
user.IsExternal = true;
(await UserManager.UpdateAsync(user)).CheckErrors();
}
protected abstract Task<ExternalLoginUserInfo> GetUserInfoAsync(string userName);
protected virtual Task<ExternalLoginUserInfo> GetUserInfoAsync(IdentityUser user)
{
return GetUserInfoAsync(user.UserName);
}
private static void NormalizeExternalLoginUserInfo(
ExternalLoginUserInfo externalUser,
string userName
)
{
if (externalUser.ProviderKey.IsNullOrWhiteSpace())
{
externalUser.ProviderKey = userName;
}
}
}
}

18
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderDictionary.cs

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Volo.Abp.Identity.AspNetCore
{
public class ExternalLoginProviderDictionary : Dictionary<string, ExternalLoginProviderInfo>
{
/// <summary>
/// Adds or replaces a provider.
/// </summary>
public void Add<TProvider>([NotNull] string name)
where TProvider : IExternalLoginProvider
{
this[name] = new ExternalLoginProviderInfo(name, typeof(TProvider));
}
}
}

25
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderInfo.cs

@ -0,0 +1,25 @@
using System;
using JetBrains.Annotations;
namespace Volo.Abp.Identity.AspNetCore
{
public class ExternalLoginProviderInfo
{
public string Name { get; }
public Type Type
{
get => _type;
set => _type = Check.NotNull(value, nameof(value));
}
private Type _type;
public ExternalLoginProviderInfo(
[NotNull] string name,
[NotNull] Type type)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
Type = Check.AssignableTo<IExternalLoginProvider>(type, nameof(type));
}
}
}

36
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginUserInfo.cs

@ -0,0 +1,36 @@
using JetBrains.Annotations;
namespace Volo.Abp.Identity.AspNetCore
{
public class ExternalLoginUserInfo
{
[CanBeNull]
public string Name { get; set; }
[CanBeNull]
public string Surname { get; set; }
[CanBeNull]
public string PhoneNumber { get; set; }
[NotNull]
public string Email { get; private set; }
[CanBeNull]
public bool? PhoneNumberConfirmed { get; set; }
[CanBeNull]
public bool? EmailConfirmed { get; set; }
[CanBeNull]
public bool? TwoFactorEnabled { get; set; }
[CanBeNull]
public string ProviderKey { get; set; }
public ExternalLoginUserInfo([System.Diagnostics.CodeAnalysis.NotNull] string email)
{
Email = Check.NotNullOrWhiteSpace(email, nameof(email));
}
}
}

32
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs

@ -0,0 +1,32 @@
using System.Threading.Tasks;
namespace Volo.Abp.Identity.AspNetCore
{
public interface IExternalLoginProvider
{
/// <summary>
/// Used to try authenticate a user by this source.
/// </summary>
/// <param name="userName">User name or email address</param>
/// <param name="plainPassword">Plain password of the user</param>
/// <returns>True, indicates that this used has authenticated by this source</returns>
Task<bool> TryAuthenticateAsync(string userName, string plainPassword);
/// <summary>
/// This method is called when a user is authenticated by this source but the user does not exists yet.
/// So, the source should create the user and fill the properties.
/// </summary>
/// <param name="userName">User name</param>
/// <param name="providerName">The name of this provider</param>
/// <returns>Newly created user</returns>
Task<IdentityUser> CreateUserAsync(string userName, string providerName);
/// <summary>
/// This method is called after an existing user is authenticated by this source.
/// It can be used to update some properties of the user by the source.
/// </summary>
/// <param name="providerName">The name of this provider</param>
/// <param name="user">The user that can be updated</param>
Task UpdateUserAsync(IdentityUser user, string providerName);
}
}

5
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs

@ -32,5 +32,10 @@ namespace Volo.Abp.Identity
/// Default value: 256 /// Default value: 256
/// </summary> /// </summary>
public static int MaxSecurityStampLength { get; set; } = 256; public static int MaxSecurityStampLength { get; set; } = 256;
/// <summary>
/// Default value: 16
/// </summary>
public static int MaxLoginProviderLength { get; set; } = 16;
} }
} }

42
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs

@ -30,11 +30,13 @@ namespace Volo.Abp.Identity
/// <summary> /// <summary>
/// Gets or sets the Name for the user. /// Gets or sets the Name for the user.
/// </summary> /// </summary>
[CanBeNull]
public virtual string Name { get; set; } public virtual string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Surname for the user. /// Gets or sets the Surname for the user.
/// </summary> /// </summary>
[CanBeNull]
public virtual string Surname { get; set; } public virtual string Surname { get; set; }
/// <summary> /// <summary>
@ -66,9 +68,12 @@ namespace Volo.Abp.Identity
[DisableAuditing] [DisableAuditing]
public virtual string SecurityStamp { get; protected internal set; } public virtual string SecurityStamp { get; protected internal set; }
public virtual bool IsExternal { get; set; }
/// <summary> /// <summary>
/// Gets or sets a telephone number for the user. /// Gets or sets a telephone number for the user.
/// </summary> /// </summary>
[CanBeNull]
public virtual string PhoneNumber { get; protected internal set; } public virtual string PhoneNumber { get; protected internal set; }
/// <summary> /// <summary>
@ -133,7 +138,11 @@ namespace Volo.Abp.Identity
{ {
} }
public IdentityUser(Guid id, [NotNull] string userName, [NotNull] string email, Guid? tenantId = null) public IdentityUser(
Guid id,
[NotNull] string userName,
[NotNull] string email,
Guid? tenantId = null)
{ {
Check.NotNull(userName, nameof(userName)); Check.NotNull(userName, nameof(userName));
Check.NotNull(email, nameof(email)); Check.NotNull(email, nameof(email));
@ -254,7 +263,8 @@ namespace Volo.Abp.Identity
Check.NotNull(loginProvider, nameof(loginProvider)); Check.NotNull(loginProvider, nameof(loginProvider));
Check.NotNull(providerKey, nameof(providerKey)); Check.NotNull(providerKey, nameof(providerKey));
Logins.RemoveAll(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey); Logins.RemoveAll(userLogin =>
userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey);
} }
[CanBeNull] [CanBeNull]
@ -316,9 +326,37 @@ namespace Volo.Abp.Identity
); );
} }
/// <summary>
/// Use <see cref="IdentityUserManager.ConfirmEmailAsync"/> for regular email confirmation.
/// Using this skips the confirmation process and directly sets the <see cref="EmailConfirmed"/>.
/// </summary>
public virtual void SetEmailConfirmed(bool confirmed)
{
EmailConfirmed = confirmed;
}
public virtual void SetPhoneNumberConfirmed(bool confirmed)
{
PhoneNumberConfirmed = confirmed;
}
public override string ToString() public override string ToString()
{ {
return $"{base.ToString()}, UserName = {UserName}"; return $"{base.ToString()}, UserName = {UserName}";
} }
/// <summary>
/// Normally use <see cref="IdentityUserManager.ChangePhoneNumberAsync"/> to change the phone number
/// in the application code.
/// This method is to directly set it with a confirmation information.
/// </summary>
/// <param name="phoneNumber"></param>
/// <param name="confirmed"></param>
/// <exception cref="NotImplementedException"></exception>
public void SetPhoneNumber(string phoneNumber, bool confirmed)
{
PhoneNumber = phoneNumber;
PhoneNumberConfirmed = !phoneNumber.IsNullOrWhiteSpace() && confirmed;
}
} }
} }

22
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs

@ -291,7 +291,7 @@ namespace Volo.Abp.Identity
/// </summary> /// </summary>
/// <param name="user">The user to retrieve the password hash for.</param> /// <param name="user">The user to retrieve the password hash for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user has a password. If the /// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user has a password. If the
/// user has a password the returned value with be true, otherwise it will be false.</returns> /// user has a password the returned value with be true, otherwise it will be false.</returns>
public virtual Task<bool> HasPasswordAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default) public virtual Task<bool> HasPasswordAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default)
{ {
@ -326,7 +326,7 @@ namespace Volo.Abp.Identity
{ {
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Role {0} does not exist!", normalizedRoleName)); throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Role {0} does not exist!", normalizedRoleName));
} }
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken);
user.AddRole(role.Id); user.AddRole(role.Id);
@ -353,7 +353,7 @@ namespace Volo.Abp.Identity
} }
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken);
user.RemoveRole(role.Id); user.RemoveRole(role.Id);
} }
@ -384,15 +384,15 @@ namespace Volo.Abp.Identity
/// <param name="user">The user whose role membership should be checked.</param> /// <param name="user">The user whose role membership should be checked.</param>
/// <param name="normalizedRoleName">The role to check membership of</param> /// <param name="normalizedRoleName">The role to check membership of</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user is a member of the given group. If the /// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user is a member of the given group. If the
/// user is a member of the group the returned value with be true, otherwise it will be false.</returns> /// user is a member of the group the returned value with be true, otherwise it will be false.</returns>
public virtual async Task<bool> IsInRoleAsync( public virtual async Task<bool> IsInRoleAsync(
[NotNull] IdentityUser user, [NotNull] IdentityUser user,
[NotNull] string normalizedRoleName, [NotNull] string normalizedRoleName,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
Check.NotNull(user, nameof(user)); Check.NotNull(user, nameof(user));
Check.NotNullOrWhiteSpace(normalizedRoleName, nameof(normalizedRoleName)); Check.NotNullOrWhiteSpace(normalizedRoleName, nameof(normalizedRoleName));
@ -589,7 +589,7 @@ namespace Volo.Abp.Identity
Check.NotNull(user, nameof(user)); Check.NotNull(user, nameof(user));
user.EmailConfirmed = confirmed; user.SetEmailConfirmed(confirmed);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -864,7 +864,7 @@ namespace Volo.Abp.Identity
Check.NotNull(user, nameof(user)); Check.NotNull(user, nameof(user));
user.PhoneNumberConfirmed = confirmed; user.SetPhoneNumberConfirmed(confirmed);
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -928,7 +928,7 @@ namespace Volo.Abp.Identity
/// <param name="user">The user whose two factor authentication enabled status should be set.</param> /// <param name="user">The user whose two factor authentication enabled status should be set.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns> /// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing a flag indicating whether the specified /// The <see cref="Task"/> that represents the asynchronous operation, containing a flag indicating whether the specified
/// <paramref name="user"/> has two factor authentication enabled or not. /// <paramref name="user"/> has two factor authentication enabled or not.
/// </returns> /// </returns>
public virtual Task<bool> GetTwoFactorEnabledAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default) public virtual Task<bool> GetTwoFactorEnabledAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default)
@ -946,7 +946,7 @@ namespace Volo.Abp.Identity
/// <param name="claim">The claim whose users should be retrieved.</param> /// <param name="claim">The claim whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns> /// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim. /// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim.
/// </returns> /// </returns>
public virtual async Task<IList<IdentityUser>> GetUsersForClaimAsync([NotNull] Claim claim, CancellationToken cancellationToken = default) public virtual async Task<IList<IdentityUser>> GetUsersForClaimAsync([NotNull] Claim claim, CancellationToken cancellationToken = default)
{ {
@ -963,7 +963,7 @@ namespace Volo.Abp.Identity
/// <param name="normalizedRoleName">The role whose users should be retrieved.</param> /// <param name="normalizedRoleName">The role whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns> /// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that are in the specified role. /// The <see cref="Task"/> contains a list of users, if any, that are in the specified role.
/// </returns> /// </returns>
public virtual async Task<IList<IdentityUser>> GetUsersInRoleAsync([NotNull] string normalizedRoleName, CancellationToken cancellationToken = default) public virtual async Task<IList<IdentityUser>> GetUsersInRoleAsync([NotNull] string normalizedRoleName, CancellationToken cancellationToken = default)
{ {

3
modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs

@ -43,6 +43,9 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
b.Property(u => u.LockoutEnabled).HasDefaultValue(false) b.Property(u => u.LockoutEnabled).HasDefaultValue(false)
.HasColumnName(nameof(IdentityUser.LockoutEnabled)); .HasColumnName(nameof(IdentityUser.LockoutEnabled));
b.Property(u => u.IsExternal).IsRequired().HasDefaultValue(false)
.HasColumnName(nameof(IdentityUser.IsExternal));
b.Property(u => u.AccessFailedCount) b.Property(u => u.AccessFailedCount)
.If(!builder.IsUsingOracle(), p => p.HasDefaultValue(0)) .If(!builder.IsUsingOracle(), p => p.HasDefaultValue(0))
.HasColumnName(nameof(IdentityUser.AccessFailedCount)); .HasColumnName(nameof(IdentityUser.AccessFailedCount));

17
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestBase.cs

@ -1,9 +1,24 @@
using Volo.Abp.AspNetCore.TestBase; using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.AspNetCore.TestBase;
namespace Volo.Abp.Identity.AspNetCore namespace Volo.Abp.Identity.AspNetCore
{ {
public abstract class AbpIdentityAspNetCoreTestBase : AbpAspNetCoreIntegratedTestBase<AbpIdentityAspNetCoreTestStartup> public abstract class AbpIdentityAspNetCoreTestBase : AbpAspNetCoreIntegratedTestBase<AbpIdentityAspNetCoreTestStartup>
{ {
protected virtual async Task<string> GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
var response = await GetResponseAsync(url, expectedStatusCode);
return await response.Content.ReadAsStringAsync();
}
protected virtual async Task<HttpResponseMessage> GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
var response = await Client.GetAsync(url);
response.StatusCode.ShouldBe(expectedStatusCode);
return response;
}
} }
} }

8
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs

@ -24,6 +24,14 @@ namespace Volo.Abp.Identity.AspNetCore
}); });
} }
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpIdentityAspNetCoreOptions>(options =>
{
options.ExternalLoginProviders.Add<FakeExternalLoginProvider>(FakeExternalLoginProvider.Name);
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)
{ {
var app = context.GetApplicationBuilder(); var app = context.GetApplicationBuilder();

24
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs

@ -1,6 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
@ -8,6 +9,13 @@ namespace Volo.Abp.Identity.AspNetCore
{ {
public class AbpSignInManager_Tests : AbpIdentityAspNetCoreTestBase public class AbpSignInManager_Tests : AbpIdentityAspNetCoreTestBase
{ {
[Fact]
public void Should_Resolve_AbpSignInManager()
{
var signInManager = GetRequiredService<SignInManager<IdentityUser>>();
signInManager.ShouldBeOfType<AbpSignInManager>();
}
[Fact] [Fact]
public async Task Should_SignIn_With_Correct_Credentials() public async Task Should_SignIn_With_Correct_Credentials()
{ {
@ -17,6 +25,7 @@ namespace Volo.Abp.Identity.AspNetCore
result.ShouldBe("Succeeded"); result.ShouldBe("Succeeded");
} }
[Fact] [Fact]
public async Task Should_Not_SignIn_With_Wrong_Credentials() public async Task Should_Not_SignIn_With_Wrong_Credentials()
{ {
@ -26,20 +35,5 @@ namespace Volo.Abp.Identity.AspNetCore
result.ShouldBe("Failed"); result.ShouldBe("Failed");
} }
//TODO: Move to a better common place ----------------------------------------------------
protected virtual async Task<string> GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
var response = await GetResponseAsync(url, expectedStatusCode);
return await response.Content.ReadAsStringAsync();
}
protected virtual async Task<HttpResponseMessage> GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
{
var response = await Client.GetAsync(url);
response.StatusCode.ShouldBe(expectedStatusCode);
return response;
}
} }
} }

57
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ExternalLoginProvider_Tests.cs

@ -0,0 +1,57 @@
using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.Identity.AspNetCore
{
public class ExternalLoginProvider_Tests : AbpIdentityAspNetCoreTestBase
{
[Fact]
public async Task Should_SignIn_With_ExternalLoginProvider()
{
// User does not exists yet
(await GetRequiredService<IdentityUserManager>().FindByNameAsync("ext_user")).ShouldBeNull();
// Try to login
var result = await GetResponseAsStringAsync(
"api/signin-test/password?userName=ext_user&password=abc"
);
result.ShouldBe("Succeeded");
// User should be created now
await CheckUserAsync();
// Re-login
result = await GetResponseAsStringAsync(
"api/signin-test/password?userName=ext_user&password=abc"
);
result.ShouldBe("Succeeded");
await CheckUserAsync();
}
private async Task CheckUserAsync()
{
var userRepository = GetRequiredService<IIdentityUserRepository>();
var user = await userRepository.FindByNormalizedUserNameAsync("EXT_USER");
user.Name.ShouldBe("Test Name");
user.Surname.ShouldBe("Test Surname");
user.EmailConfirmed.ShouldBeTrue();
user.TwoFactorEnabled.ShouldBeFalse();
user.PhoneNumber.ShouldBe("123");
user.PhoneNumberConfirmed.ShouldBeFalse();
user.IsExternal.ShouldBeTrue();
var logins = user.Logins.Where(l => l.LoginProvider == "Fake").ToList();
logins.Count.ShouldBe(1);
logins[0].ProviderKey.ShouldBe("123");
}
}
}

55
modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeExternalLoginProvider.cs

@ -0,0 +1,55 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Identity.AspNetCore
{
public class FakeExternalLoginProvider : ExternalLoginProviderBase, ITransientDependency
{
public const string Name = "Fake";
public FakeExternalLoginProvider(
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant,
IdentityUserManager userManager,
IIdentityUserRepository identityUserRepository)
: base(
guidGenerator,
currentTenant,
userManager,
identityUserRepository)
{
}
public override Task<bool> TryAuthenticateAsync(string userName, string plainPassword)
{
return Task.FromResult(
userName == "ext_user" && plainPassword == "abc"
);
}
protected override Task<ExternalLoginUserInfo> GetUserInfoAsync(string userName)
{
if (userName != "ext_user")
{
throw new ArgumentException();
}
return Task.FromResult(
new ExternalLoginUserInfo("ext_user@test.com")
{
Name = "Test Name", //optional, if the provider knows it
Surname = "Test Surname", //optional, if the provider knows it
EmailConfirmed = true, //optional, if the provider knows it
TwoFactorEnabled = false, //optional, if the provider knows it
PhoneNumber = "123", //optional, if the provider knows it
PhoneNumberConfirmed = false, //optional, if the provider knows it
ProviderKey = "123" //The id of the user on the provider side
}
);
}
}
}

2
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityClaimTypeManager_Tests.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Shouldly; using Shouldly;
using Xunit; using Xunit;

8
templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.Designer.cs → templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.Designer.cs

@ -11,7 +11,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace MyCompanyName.MyProjectName.Migrations namespace MyCompanyName.MyProjectName.Migrations
{ {
[DbContext(typeof(MyProjectNameMigrationsDbContext))] [DbContext(typeof(MyProjectNameMigrationsDbContext))]
[Migration("20200710064926_Initial")] [Migration("20200806191520_Initial")]
partial class Initial partial class Initial
{ {
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -619,6 +619,12 @@ namespace MyCompanyName.MyProjectName.Migrations
.HasColumnType("bit") .HasColumnType("bit")
.HasDefaultValue(false); .HasDefaultValue(false);
b.Property<bool>("IsExternal")
.ValueGeneratedOnAdd()
.HasColumnName("IsExternal")
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
.HasColumnName("LastModificationTime") .HasColumnName("LastModificationTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");

1
templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.cs → templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.cs

@ -244,6 +244,7 @@ namespace MyCompanyName.MyProjectName.Migrations
EmailConfirmed = table.Column<bool>(nullable: false, defaultValue: false), EmailConfirmed = table.Column<bool>(nullable: false, defaultValue: false),
PasswordHash = table.Column<string>(maxLength: 256, nullable: true), PasswordHash = table.Column<string>(maxLength: 256, nullable: true),
SecurityStamp = table.Column<string>(maxLength: 256, nullable: false), SecurityStamp = table.Column<string>(maxLength: 256, nullable: false),
IsExternal = table.Column<bool>(nullable: false, defaultValue: false),
PhoneNumber = table.Column<string>(maxLength: 16, nullable: true), PhoneNumber = table.Column<string>(maxLength: 16, nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false, defaultValue: false), PhoneNumberConfirmed = table.Column<bool>(nullable: false, defaultValue: false),
TwoFactorEnabled = table.Column<bool>(nullable: false, defaultValue: false), TwoFactorEnabled = table.Column<bool>(nullable: false, defaultValue: false),

6
templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/MyProjectNameMigrationsDbContextModelSnapshot.cs

@ -617,6 +617,12 @@ namespace MyCompanyName.MyProjectName.Migrations
.HasColumnType("bit") .HasColumnType("bit")
.HasDefaultValue(false); .HasDefaultValue(false);
b.Property<bool>("IsExternal")
.ValueGeneratedOnAdd()
.HasColumnName("IsExternal")
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTime?>("LastModificationTime") b.Property<DateTime?>("LastModificationTime")
.HasColumnName("LastModificationTime") .HasColumnName("LastModificationTime")
.HasColumnType("datetime2"); .HasColumnType("datetime2");

Loading…
Cancel
Save