From 5f78f89a53548a62161c9c1180411678968a469a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Thu, 30 Jul 2020 17:21:17 +0300 Subject: [PATCH] #4927: Implement social/external logins for the account module. --- .../Account/Localization/Resources/en.json | 3 +- .../Pages/Account/Login.cshtml | 81 +++++----- .../Pages/Account/Login.cshtml.cs | 21 ++- .../Pages/Account/Register.cshtml | 37 +++-- .../Pages/Account/Register.cshtml.cs | 141 ++++++++++++++++-- 5 files changed, 212 insertions(+), 71 deletions(-) diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json index f0dbdaadb6..2940fff63b 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json @@ -43,6 +43,7 @@ "Description:Abp.Account.EnableLocalLogin": "Indicates if the server will allow users to authenticate with a local account.", "LoggedOutTitle": "Signed Out", "LoggedOutText": "You have been signed out and you will be redirected soon.", - "ReturnToText": "Click here to redirect to {0}" + "ReturnToText": "Click here to redirect to {0}", + "OrLoginWith": "Or login with;" } } diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml index 3bace4850b..41986c3856 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml @@ -6,34 +6,34 @@ @model Volo.Abp.Account.Web.Pages.Account.LoginModel @inject IHtmlLocalizer L @inject Volo.Abp.Settings.ISettingProvider SettingProvider -@if (Model.EnableLocalLogin) -{ -
-
-

@L["Login"]

- @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) - { - - @L["AreYouANewUser"] - @L["Register"] - - } +
+
+

@L["Login"]

+ @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) + { + + @L["AreYouANewUser"] + @L["Register"] + + } + @if (Model.EnableLocalLogin) + {
- - + +
- +
- +
@@ -43,29 +43,30 @@ @L["Cancel"] }
-
-
-} + } -@if (Model.VisibleExternalProviders.Any()) -{ -
-

@L["UseAnotherServiceToLogIn"]

-
- - - @foreach (var provider in Model.VisibleExternalProviders) - { - - } -
-
-} + @if (Model.VisibleExternalProviders.Any()) + { +
+
@L["OrLoginWith"]
+
+ + + @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
+
+ } + + @if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) + { +
+ @L["InvalidLoginRequest"] + @L["ThereAreNoLoginSchemesConfiguredForThisClient"] +
+ } -@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) -{ -
- @L["InvalidLoginRequest"] - @L["ThereAreNoLoginSchemesConfiguredForThisClient"]
-} +
diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs index 9d6c540ce5..ccd6e07a0f 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -214,13 +214,23 @@ namespace Volo.Abp.Account.Web.Pages.Account //TODO: Handle other cases for result! // Get the information about the user from the external login provider - var info = await SignInManager.GetExternalLoginInfoAsync(); - if (info == null) + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) { throw new ApplicationException("Error loading external login information during confirmation."); } - var user = await CreateExternalUserAsync(info); + if (!IsEmailRetrievedFromExternalLogin(externalLoginInfo)) + { + return RedirectToPage("./Register", new + { + IsExternalLogin = true, + ExternalLoginAuthSchema = externalLoginInfo.LoginProvider, + ReturnUrl = returnUrl + }); + } + + var user = await CreateExternalUserAsync(externalLoginInfo); await SignInManager.SignInAsync(user, false); @@ -234,6 +244,11 @@ namespace Volo.Abp.Account.Web.Pages.Account return RedirectSafely(returnUrl, returnUrlHash); } + private static bool IsEmailRetrievedFromExternalLogin(ExternalLoginInfo externalLoginInfo) + { + return externalLoginInfo.Principal.FindFirstValue(AbpClaimTypes.Email) != null; + } + protected virtual async Task CreateExternalUserAsync(ExternalLoginInfo info) { var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email); diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml index 204540e7e7..42779e864c 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml @@ -4,18 +4,27 @@ @model Volo.Abp.Account.Web.Pages.Account.RegisterModel @inject IHtmlLocalizer L -
-
-

@L["Register"]

- - @L["AlreadyRegistered"] - @L["Login"] - -
- - - - @L["Register"] - -
+
+
+

@L["Register"]

+ + @L["AlreadyRegistered"] + @L["Login"] + +
+ @if (!Model.IsExternalLogin) + { + + } + + + + @if (!Model.IsExternalLogin) + { + + } + + @L["Register"] +
+
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 f6d626dd63..6baefc6745 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 @@ -1,14 +1,16 @@ +using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Security.Claims; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Volo.Abp.Account.Settings; using Volo.Abp.Auditing; -using Volo.Abp.Application.Dtos; using Volo.Abp.Identity; using Volo.Abp.Settings; -using Volo.Abp.Uow; using Volo.Abp.Validation; using IdentityUser = Volo.Abp.Identity.IdentityUser; @@ -27,6 +29,12 @@ namespace Volo.Abp.Account.Web.Pages.Account [BindProperty] public PostInput Input { get; set; } + [BindProperty(SupportsGet = true)] + public bool IsExternalLogin { get; set; } + + [BindProperty(SupportsGet = true)] + public string ExternalLoginAuthSchema { get; set; } + public RegisterModel(IAccountAppService accountAppService) { AccountAppService = accountAppService; @@ -35,34 +43,141 @@ namespace Volo.Abp.Account.Web.Pages.Account public virtual async Task OnGetAsync() { await CheckSelfRegistrationAsync(); - + await TrySetEmailAsync(); return Page(); } - public virtual async Task OnPostAsync() + private async Task TrySetEmailAsync() { - ValidateModel(); + if (IsExternalLogin) + { + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) + { + return; + } + + if (!externalLoginInfo.Principal.Identities.Any()) + { + return; + } + + var identity = externalLoginInfo.Principal.Identities.First(); + var emailClaim = identity.FindFirst(ClaimTypes.Email); + + if (emailClaim == null) + { + return; + } + + Input = new PostInput {EmailAddress = emailClaim.Value}; + } + } + public virtual async Task OnPostAsync() + { await CheckSelfRegistrationAsync(); - var registerDto = new RegisterDto + var registerDto = new RegisterDto() { - AppName = "MVC", - EmailAddress = Input.EmailAddress, - Password = Input.Password, - UserName = Input.UserName + AppName = "MVC" }; - var userDto = await AccountAppService.RegisterAsync(registerDto); - var user = await UserManager.GetByIdAsync(userDto.Id); + if (IsExternalLogin) + { + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) + { + Logger.LogWarning("External login info is not available"); + return RedirectToPage("./Login"); + } + + registerDto.EmailAddress = Input.EmailAddress; + registerDto.UserName = Input.EmailAddress; + registerDto.Password = GeneratePassword(); + } + else + { + ValidateModel(); - await UserManager.SetEmailAsync(user, Input.EmailAddress); + 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! } + protected virtual async Task AddToUserLogins(IdentityUser user) + { + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + + var userLoginAlreadyExists = user.Logins.Any(x => + x.TenantId == user.TenantId && + x.LoginProvider == externalLoginInfo.LoginProvider && + x.ProviderKey == externalLoginInfo.ProviderKey); + + if (!userLoginAlreadyExists) + { + user.AddLogin(new UserLoginInfo( + externalLoginInfo.LoginProvider, + externalLoginInfo.ProviderKey, + externalLoginInfo.ProviderDisplayName + ) + ); + } + } + + protected virtual string GeneratePassword() + { + var random = new Random(); + var options = UserManager.Options.Password; + int length = random.Next(options.RequiredLength, IdentityUserConsts.MaxPasswordLength - 1); + + bool nonAlphanumeric = options.RequireNonAlphanumeric; + bool digit = options.RequireDigit; + bool lowercase = options.RequireLowercase; + bool uppercase = options.RequireUppercase; + + StringBuilder password = new StringBuilder(); + + while (password.Length < length) + { + 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; + } + + 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(); + } + protected virtual async Task CheckSelfRegistrationAsync() { if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled) ||