mirror of https://github.com/abpframework/abp.git
Browse Source
Implemented: Authentication Extensibility system to check username & password from an external sourcepull/4993/head
committed by
GitHub
22 changed files with 592 additions and 104 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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
|
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue