From 01580e3aa71ca0b5c0d34175b8b5eb3f3cc12f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 6 Aug 2020 15:12:56 +0300 Subject: [PATCH] Introduce IExternalLoginProvider --- .../AbpIdentityAspNetCoreOptions.cs | 9 ++++- .../Identity/AspNetCore/AbpSignInManager.cs | 38 +++++++++++++++++-- .../ExternalLoginProviderDictionary.cs | 18 +++++++++ .../AspNetCore/ExternalLoginProviderInfo.cs | 25 ++++++++++++ .../AspNetCore/IExternalLoginProvider.cs | 30 +++++++++++++++ .../AbpIdentityAspNetCoreTestBase.cs | 17 ++++++++- .../AbpIdentityAspNetCoreTestModule.cs | 8 ++++ .../AspNetCore/AbpSignInManager_Tests.cs | 15 -------- .../AspNetCore/ExternalLoginProvider_Tests.cs | 19 ++++++++++ .../AspNetCore/FakeExternalLoginProvider.cs | 34 +++++++++++++++++ 10 files changed, 192 insertions(+), 21 deletions(-) create mode 100644 modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderDictionary.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/ExternalLoginProviderInfo.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ExternalLoginProvider_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeExternalLoginProvider.cs 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 index 30f9407e6e..85e39ff8c7 100644 --- 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 @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -9,6 +10,8 @@ namespace Volo.Abp.Identity.AspNetCore { public class AbpSignInManager : SignInManager { + protected AbpIdentityAspNetCoreOptions AbpOptions { get; } + public AbpSignInManager( UserManager userManager, IHttpContextAccessor contextAccessor, @@ -16,7 +19,8 @@ namespace Volo.Abp.Identity.AspNetCore IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, - IUserConfirmation confirmation + IUserConfirmation confirmation, + IOptions options ) : base( userManager, contextAccessor, @@ -26,16 +30,42 @@ namespace Volo.Abp.Identity.AspNetCore schemes, confirmation) { - + AbpOptions = options.Value; } - public override Task PasswordSignInAsync( + public override async Task PasswordSignInAsync( string userName, string password, bool isPersistent, bool lockoutOnFailure) { - return base.PasswordSignInAsync(userName, password, isPersistent, 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); + //TODO: TenantId, LoginProvider, Password, NormalizeNames + //TODO: Set default roles + await UserManager.CreateAsync(user); + } + else + { + await externalLoginProvider.UpdateUserAsync(user); + //TODO: LoginProvider + await UserManager.UpdateAsync(user); + } + + 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/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/IExternalLoginProvider.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs new file mode 100644 index 0000000000..eabed8d628 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/IExternalLoginProvider.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Identity.AspNetCore +{ + public interface IExternalLoginProvider //TODO: A base class to simplift implementing this! + { + /// + /// 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 + /// Newly created user + Task CreateUserAsync(string userName); + + /// + /// 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 user that can be updated + Task UpdateUserAsync(IdentityUser user); + } +} 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 212c341733..a99f2d7212 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 @@ -35,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..89e12d47a1 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/ExternalLoginProvider_Tests.cs @@ -0,0 +1,19 @@ +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() + { + var result = await GetResponseAsStringAsync( + "api/signin-test/password?userName=ext_user&password=abc" + ); + + result.ShouldBe("Succeeded"); + } + } +} 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..8dacdae65e --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/FakeExternalLoginProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Identity.AspNetCore +{ + public class FakeExternalLoginProvider : IExternalLoginProvider, ITransientDependency + { + public const string Name = "Fake"; + + public Task TryAuthenticateAsync(string userName, string plainPassword) + { + return Task.FromResult( + userName == "ext_user" && plainPassword == "abc" + ); + } + + public Task CreateUserAsync(string userName) + { + return Task.FromResult( + new IdentityUser( + Guid.NewGuid(), + userName, + "test@abp.io" + ) + ); + } + + public Task UpdateUserAsync(IdentityUser user) + { + return Task.CompletedTask; + } + } +}