diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs index 6baefc6745..536c17ab26 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs +++ b/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(); - var registerDto = new RegisterDto() - { - AppName = "MVC" - }; - if (IsExternalLogin) { var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); @@ -92,90 +87,56 @@ namespace Volo.Abp.Account.Web.Pages.Account return RedirectToPage("./Login"); } - registerDto.EmailAddress = Input.EmailAddress; - registerDto.UserName = Input.EmailAddress; - registerDto.Password = GeneratePassword(); + await RegisterExternalUserAsync(externalLoginInfo, Input.EmailAddress); } else { - ValidateModel(); - - 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); + await RegisterLocalUserAsync(); } 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 => - x.TenantId == user.TenantId && - x.LoginProvider == externalLoginInfo.LoginProvider && - x.ProviderKey == externalLoginInfo.ProviderKey); + var userDto = await AccountAppService.RegisterAsync( + new RegisterDto + { + AppName = "MVC", + EmailAddress = Input.EmailAddress, + Password = Input.Password, + UserName = Input.UserName + } + ); - if (!userLoginAlreadyExists) - { - user.AddLogin(new UserLoginInfo( - externalLoginInfo.LoginProvider, - externalLoginInfo.ProviderKey, - externalLoginInfo.ProviderDisplayName - ) - ); - } + var user = await UserManager.GetByIdAsync(userDto.Id); + await SignInManager.SignInAsync(user, isPersistent: true); } - protected virtual string GeneratePassword() + protected virtual async Task RegisterExternalUserAsync(ExternalLoginInfo externalLoginInfo, string emailAddress) { - var random = new Random(); - var options = UserManager.Options.Password; - int length = random.Next(options.RequiredLength, IdentityUserConsts.MaxPasswordLength - 1); + var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id); - bool nonAlphanumeric = options.RequireNonAlphanumeric; - bool digit = options.RequireDigit; - bool lowercase = options.RequireLowercase; - bool uppercase = options.RequireUppercase; + (await UserManager.CreateAsync(user)).CheckErrors(); + (await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); - 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); - - password.Append(c); - - if (char.IsDigit(c)) - digit = false; - else if (char.IsLower(c)) - lowercase = false; - else if (char.IsUpper(c)) - uppercase = false; - else if (!char.IsLetterOrDigit(c)) - nonAlphanumeric = false; + (await UserManager.AddLoginAsync(user, new UserLoginInfo( + externalLoginInfo.LoginProvider, + externalLoginInfo.ProviderKey, + externalLoginInfo.ProviderDisplayName + ))).CheckErrors(); } - if (nonAlphanumeric) - 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(); + await SignInManager.SignInAsync(user, isPersistent: true); } protected virtual async Task CheckSelfRegistrationAsync() diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs index c0e3031568..019ef0469e 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs @@ -13,8 +13,9 @@ namespace Volo.Abp.Identity.AspNetCore { PreConfigure(builder => { - builder.AddDefaultTokenProviders(); - builder.AddSignInManager(); + builder + .AddDefaultTokenProviders() + .AddSignInManager(); }); } diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreOptions.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreOptions.cs index ad42a23f9a..d7c5f57265 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreOptions.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreOptions.cs @@ -6,5 +6,12 @@ /// Default: true. /// public bool ConfigureAuthentication { get; set; } = true; + + public ExternalLoginProviderDictionary ExternalLoginProviders { get; } + + public AbpIdentityAspNetCoreOptions() + { + ExternalLoginProviders = new ExternalLoginProviderDictionary(); + } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs new file mode 100644 index 0000000000..f9072be6e8 --- /dev/null +++ b/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 + { + protected AbpIdentityAspNetCoreOptions AbpOptions { get; } + + public AbpSignInManager( + IdentityUserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation, + IOptions options + ) : base( + userManager, + contextAccessor, + claimsFactory, + optionsAccessor, + logger, + schemes, + confirmation) + { + AbpOptions = options.Value; + } + + public override async Task 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); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderBase.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderBase.cs new file mode 100644 index 0000000000..d1db82e254 --- /dev/null +++ b/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 TryAuthenticateAsync(string userName, string plainPassword); + + public virtual async Task 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 GetUserInfoAsync(string userName); + + protected virtual Task GetUserInfoAsync(IdentityUser user) + { + return GetUserInfoAsync(user.UserName); + } + + private static void NormalizeExternalLoginUserInfo( + ExternalLoginUserInfo externalUser, + string userName + ) + { + if (externalUser.ProviderKey.IsNullOrWhiteSpace()) + { + externalUser.ProviderKey = userName; + } + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderDictionary.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderDictionary.cs new file mode 100644 index 0000000000..bca983cb86 --- /dev/null +++ b/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 + { + /// + /// Adds or replaces a provider. + /// + public void Add([NotNull] string name) + where TProvider : IExternalLoginProvider + { + this[name] = new ExternalLoginProviderInfo(name, typeof(TProvider)); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderInfo.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderInfo.cs new file mode 100644 index 0000000000..86ef94d58f --- /dev/null +++ b/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(type, nameof(type)); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginUserInfo.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginUserInfo.cs new file mode 100644 index 0000000000..530a9bcfe0 --- /dev/null +++ b/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)); + } + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs new file mode 100644 index 0000000000..29d0f0df3c --- /dev/null +++ b/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 + { + /// + /// Used to try authenticate a user by this source. + /// + /// User name or email address + /// Plain password of the user + /// True, indicates that this used has authenticated by this source + Task TryAuthenticateAsync(string userName, string plainPassword); + + /// + /// 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. + /// + /// User name + /// The name of this provider + /// Newly created user + Task CreateUserAsync(string userName, string providerName); + + /// + /// 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. + /// + /// The name of this provider + /// The user that can be updated + Task UpdateUserAsync(IdentityUser user, string providerName); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs index c374cbc678..01c6e285a9 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs @@ -32,5 +32,10 @@ namespace Volo.Abp.Identity /// Default value: 256 /// public static int MaxSecurityStampLength { get; set; } = 256; + + /// + /// Default value: 16 + /// + public static int MaxLoginProviderLength { get; set; } = 16; } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs index ea1f5ccf72..287328573d 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs @@ -30,11 +30,13 @@ namespace Volo.Abp.Identity /// /// Gets or sets the Name for the user. /// + [CanBeNull] public virtual string Name { get; set; } /// /// Gets or sets the Surname for the user. /// + [CanBeNull] public virtual string Surname { get; set; } /// @@ -66,9 +68,12 @@ namespace Volo.Abp.Identity [DisableAuditing] public virtual string SecurityStamp { get; protected internal set; } + public virtual bool IsExternal { get; set; } + /// /// Gets or sets a telephone number for the user. /// + [CanBeNull] public virtual string PhoneNumber { get; protected internal set; } /// @@ -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(email, nameof(email)); @@ -254,7 +263,8 @@ namespace Volo.Abp.Identity Check.NotNull(loginProvider, nameof(loginProvider)); Check.NotNull(providerKey, nameof(providerKey)); - Logins.RemoveAll(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey); + Logins.RemoveAll(userLogin => + userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey); } [CanBeNull] @@ -316,9 +326,37 @@ namespace Volo.Abp.Identity ); } + /// + /// Use for regular email confirmation. + /// Using this skips the confirmation process and directly sets the . + /// + public virtual void SetEmailConfirmed(bool confirmed) + { + EmailConfirmed = confirmed; + } + + public virtual void SetPhoneNumberConfirmed(bool confirmed) + { + PhoneNumberConfirmed = confirmed; + } + public override string ToString() { return $"{base.ToString()}, UserName = {UserName}"; } + + /// + /// Normally use to change the phone number + /// in the application code. + /// This method is to directly set it with a confirmation information. + /// + /// + /// + /// + public void SetPhoneNumber(string phoneNumber, bool confirmed) + { + PhoneNumber = phoneNumber; + PhoneNumberConfirmed = !phoneNumber.IsNullOrWhiteSpace() && confirmed; + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs index 81269adf5a..17a0fbd39d 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserStore.cs @@ -291,7 +291,7 @@ namespace Volo.Abp.Identity /// /// The user to retrieve the password hash for. /// The used to propagate notifications that the operation should be canceled. - /// A containing a flag indicating if the specified user has a password. If the + /// A 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. public virtual Task 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)); } - + await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); user.AddRole(role.Id); @@ -353,7 +353,7 @@ namespace Volo.Abp.Identity } await UserRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); - + user.RemoveRole(role.Id); } @@ -384,15 +384,15 @@ namespace Volo.Abp.Identity /// The user whose role membership should be checked. /// The role to check membership of /// The used to propagate notifications that the operation should be canceled. - /// A containing a flag indicating if the specified user is a member of the given group. If the + /// A 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. public virtual async Task IsInRoleAsync( - [NotNull] IdentityUser user, + [NotNull] IdentityUser user, [NotNull] string normalizedRoleName, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - + Check.NotNull(user, nameof(user)); Check.NotNullOrWhiteSpace(normalizedRoleName, nameof(normalizedRoleName)); @@ -589,7 +589,7 @@ namespace Volo.Abp.Identity Check.NotNull(user, nameof(user)); - user.EmailConfirmed = confirmed; + user.SetEmailConfirmed(confirmed); return Task.CompletedTask; } @@ -864,7 +864,7 @@ namespace Volo.Abp.Identity Check.NotNull(user, nameof(user)); - user.PhoneNumberConfirmed = confirmed; + user.SetPhoneNumberConfirmed(confirmed); return Task.CompletedTask; } @@ -928,7 +928,7 @@ namespace Volo.Abp.Identity /// The user whose two factor authentication enabled status should be set. /// The used to propagate notifications that the operation should be canceled. /// - /// The that represents the asynchronous operation, containing a flag indicating whether the specified + /// The that represents the asynchronous operation, containing a flag indicating whether the specified /// has two factor authentication enabled or not. /// public virtual Task GetTwoFactorEnabledAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default) @@ -946,7 +946,7 @@ namespace Volo.Abp.Identity /// The claim whose users should be retrieved. /// The used to propagate notifications that the operation should be canceled. /// - /// The contains a list of users, if any, that contain the specified claim. + /// The contains a list of users, if any, that contain the specified claim. /// public virtual async Task> GetUsersForClaimAsync([NotNull] Claim claim, CancellationToken cancellationToken = default) { @@ -963,7 +963,7 @@ namespace Volo.Abp.Identity /// The role whose users should be retrieved. /// The used to propagate notifications that the operation should be canceled. /// - /// The contains a list of users, if any, that are in the specified role. + /// The contains a list of users, if any, that are in the specified role. /// public virtual async Task> GetUsersInRoleAsync([NotNull] string normalizedRoleName, CancellationToken cancellationToken = default) { diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 373abbe804..8efe82c5ce 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/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) .HasColumnName(nameof(IdentityUser.LockoutEnabled)); + b.Property(u => u.IsExternal).IsRequired().HasDefaultValue(false) + .HasColumnName(nameof(IdentityUser.IsExternal)); + b.Property(u => u.AccessFailedCount) .If(!builder.IsUsingOracle(), p => p.HasDefaultValue(0)) .HasColumnName(nameof(IdentityUser.AccessFailedCount)); diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestBase.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestBase.cs index a66bb129bb..c9e11f49b3 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestBase.cs +++ b/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 { public abstract class AbpIdentityAspNetCoreTestBase : AbpAspNetCoreIntegratedTestBase { + protected virtual async Task GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK) + { + var response = await GetResponseAsync(url, expectedStatusCode); + return await response.Content.ReadAsStringAsync(); + } + protected virtual async Task GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK) + { + var response = await Client.GetAsync(url); + response.StatusCode.ShouldBe(expectedStatusCode); + return response; + } } } diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs index 79815d3941..7b39096a0f 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreTestModule.cs +++ b/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(options => + { + options.ExternalLoginProviders.Add(FakeExternalLoginProvider.Name); + }); + } + public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs index 268bfae40c..dc5d961f9a 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpSignInManager_Tests.cs +++ b/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.Http; using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Shouldly; using Xunit; @@ -8,6 +9,13 @@ namespace Volo.Abp.Identity.AspNetCore { public class AbpSignInManager_Tests : AbpIdentityAspNetCoreTestBase { + [Fact] + public void Should_Resolve_AbpSignInManager() + { + var signInManager = GetRequiredService>(); + signInManager.ShouldBeOfType(); + } + [Fact] public async Task Should_SignIn_With_Correct_Credentials() { @@ -17,6 +25,7 @@ namespace Volo.Abp.Identity.AspNetCore result.ShouldBe("Succeeded"); } + [Fact] public async Task Should_Not_SignIn_With_Wrong_Credentials() { @@ -26,20 +35,5 @@ namespace Volo.Abp.Identity.AspNetCore result.ShouldBe("Failed"); } - - //TODO: Move to a better common place ---------------------------------------------------- - - protected virtual async Task GetResponseAsStringAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK) - { - var response = await GetResponseAsync(url, expectedStatusCode); - return await response.Content.ReadAsStringAsync(); - } - - protected virtual async Task GetResponseAsync(string url, HttpStatusCode expectedStatusCode = HttpStatusCode.OK) - { - var response = await Client.GetAsync(url); - response.StatusCode.ShouldBe(expectedStatusCode); - return response; - } } } diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ExternalLoginProvider_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ExternalLoginProvider_Tests.cs new file mode 100644 index 0000000000..ee9a56432e --- /dev/null +++ b/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().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(); + + 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"); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeExternalLoginProvider.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeExternalLoginProvider.cs new file mode 100644 index 0000000000..6a54ccf7e3 --- /dev/null +++ b/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 TryAuthenticateAsync(string userName, string plainPassword) + { + return Task.FromResult( + userName == "ext_user" && plainPassword == "abc" + ); + } + + protected override Task 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 + } + ); + } + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityClaimTypeManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityClaimTypeManager_Tests.cs index c5c68d40c6..1ef603e6b3 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityClaimTypeManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityClaimTypeManager_Tests.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Shouldly; using Xunit; diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.Designer.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.Designer.cs similarity index 99% rename from templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.Designer.cs rename to templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.Designer.cs index e2ca97db45..e5df7f6948 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.Designer.cs +++ b/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 { [DbContext(typeof(MyProjectNameMigrationsDbContext))] - [Migration("20200710064926_Initial")] + [Migration("20200806191520_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -619,6 +619,12 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("bit") .HasDefaultValue(false); + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnName("IsExternal") + .HasColumnType("bit") + .HasDefaultValue(false); + b.Property("LastModificationTime") .HasColumnName("LastModificationTime") .HasColumnType("datetime2"); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.cs similarity index 99% rename from templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.cs rename to templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.cs index 2c82babc5b..57d7dadb66 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200710064926_Initial.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/20200806191520_Initial.cs @@ -244,6 +244,7 @@ namespace MyCompanyName.MyProjectName.Migrations EmailConfirmed = table.Column(nullable: false, defaultValue: false), PasswordHash = table.Column(maxLength: 256, nullable: true), SecurityStamp = table.Column(maxLength: 256, nullable: false), + IsExternal = table.Column(nullable: false, defaultValue: false), PhoneNumber = table.Column(maxLength: 16, nullable: true), PhoneNumberConfirmed = table.Column(nullable: false, defaultValue: false), TwoFactorEnabled = table.Column(nullable: false, defaultValue: false), diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/MyProjectNameMigrationsDbContextModelSnapshot.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/MyProjectNameMigrationsDbContextModelSnapshot.cs index f06dda1edc..305502aa64 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/MyProjectNameMigrationsDbContextModelSnapshot.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore.DbMigrations/Migrations/MyProjectNameMigrationsDbContextModelSnapshot.cs @@ -617,6 +617,12 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("bit") .HasDefaultValue(false); + b.Property("IsExternal") + .ValueGeneratedOnAdd() + .HasColumnName("IsExternal") + .HasColumnType("bit") + .HasDefaultValue(false); + b.Property("LastModificationTime") .HasColumnName("LastModificationTime") .HasColumnType("datetime2");