Browse Source

#4927: Implement social/external logins for the account module.

pull/4947/head
Halil İbrahim Kalkan 6 years ago
parent
commit
5f78f89a53
  1. 3
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json
  2. 81
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml
  3. 21
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  4. 37
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml
  5. 141
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

3
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.", "Description:Abp.Account.EnableLocalLogin": "Indicates if the server will allow users to authenticate with a local account.",
"LoggedOutTitle": "Signed Out", "LoggedOutTitle": "Signed Out",
"LoggedOutText": "You have been signed out and you will be redirected soon.", "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;"
} }
} }

81
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml

@ -6,34 +6,34 @@
@model Volo.Abp.Account.Web.Pages.Account.LoginModel @model Volo.Abp.Account.Web.Pages.Account.LoginModel
@inject IHtmlLocalizer<AccountResource> L @inject IHtmlLocalizer<AccountResource> L
@inject Volo.Abp.Settings.ISettingProvider SettingProvider @inject Volo.Abp.Settings.ISettingProvider SettingProvider
@if (Model.EnableLocalLogin) <div class="card mt-3 shadow-sm rounded">
{ <div class="card-body p-5">
<div class="card mt-3 shadow-sm rounded"> <h4>@L["Login"]</h4>
<div class="card-body p-5"> @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled))
<h4>@L["Login"]</h4> {
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) <strong>
{ @L["AreYouANewUser"]
<strong> <a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a>
@L["AreYouANewUser"] </strong>
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> }
</strong> @if (Model.EnableLocalLogin)
} {
<form method="post" class="mt-4"> <form method="post" class="mt-4">
<input asp-for="ReturnUrl" /> <input asp-for="ReturnUrl"/>
<input asp-for="ReturnUrlHash" /> <input asp-for="ReturnUrlHash"/>
<div class="form-group"> <div class="form-group">
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> <label asp-for="LoginInput.UserNameOrEmailAddress"></label>
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> <input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control"/>
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> <span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="LoginInput.Password"></label> <label asp-for="LoginInput.Password"></label>
<input asp-for="LoginInput.Password" class="form-control" /> <input asp-for="LoginInput.Password" class="form-control"/>
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> <span asp-validation-for="LoginInput.Password" class="text-danger"></span>
</div> </div>
<div class="form-check"> <div class="form-check">
<label asp-for="LoginInput.RememberMe" class="form-check-label"> <label asp-for="LoginInput.RememberMe" class="form-check-label">
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> <input asp-for="LoginInput.RememberMe" class="form-check-input"/>
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) @Html.DisplayNameFor(m => m.LoginInput.RememberMe)
</label> </label>
</div> </div>
@ -43,29 +43,30 @@
<abp-button type="submit" button-type="Secondary" formnovalidate="formnovalidate" name="Action" value="Cancel" class="btn-block btn-lg mt-3">@L["Cancel"]</abp-button> <abp-button type="submit" button-type="Secondary" formnovalidate="formnovalidate" name="Action" value="Cancel" class="btn-block btn-lg mt-3">@L["Cancel"]</abp-button>
} }
</form> </form>
</div> }
</div>
}
@if (Model.VisibleExternalProviders.Any()) @if (Model.VisibleExternalProviders.Any())
{ {
<div class="col-md-6"> <div class="mt-2">
<h4>@L["UseAnotherServiceToLogIn"]</h4> <h5>@L["OrLoginWith"]</h5>
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> <form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
<input asp-for="ReturnUrl" /> <input asp-for="ReturnUrl"/>
<input asp-for="ReturnUrlHash" /> <input asp-for="ReturnUrlHash"/>
@foreach (var provider in Model.VisibleExternalProviders) @foreach (var provider in Model.VisibleExternalProviders)
{ {
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> <button type="submit" class="btn btn-primary m-1" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button>
} }
</form> </form>
</div> </div>
} }
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>@L["InvalidLoginRequest"]</strong>
@L["ThereAreNoLoginSchemesConfiguredForThisClient"]
</div>
}
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>@L["InvalidLoginRequest"]</strong>
@L["ThereAreNoLoginSchemesConfiguredForThisClient"]
</div> </div>
} </div>

21
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! //TODO: Handle other cases for result!
// Get the information about the user from the external login provider // Get the information about the user from the external login provider
var info = await SignInManager.GetExternalLoginInfoAsync(); var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
if (info == null) if (externalLoginInfo == null)
{ {
throw new ApplicationException("Error loading external login information during confirmation."); 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); await SignInManager.SignInAsync(user, false);
@ -234,6 +244,11 @@ namespace Volo.Abp.Account.Web.Pages.Account
return RedirectSafely(returnUrl, returnUrlHash); return RedirectSafely(returnUrl, returnUrlHash);
} }
private static bool IsEmailRetrievedFromExternalLogin(ExternalLoginInfo externalLoginInfo)
{
return externalLoginInfo.Principal.FindFirstValue(AbpClaimTypes.Email) != null;
}
protected virtual async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info) protected virtual async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{ {
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email); var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);

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

@ -4,18 +4,27 @@
@model Volo.Abp.Account.Web.Pages.Account.RegisterModel @model Volo.Abp.Account.Web.Pages.Account.RegisterModel
@inject IHtmlLocalizer<AccountResource> L @inject IHtmlLocalizer<AccountResource> L
<div class="card mt-3 shadow-sm rounded"> <div class="card mt-3 shadow-sm rounded">
<div class="card-body p-5"> <div class="card-body p-5">
<h4>@L["Register"]</h4> <h4>@L["Register"]</h4>
<strong> <strong>
@L["AlreadyRegistered"] @L["AlreadyRegistered"]
<a href="@Url.Page("./Login", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Login"]</a> <a href="@Url.Page("./Login", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Login"]</a>
</strong> </strong>
<form method="post" class="mt-4"> <form method="post" class="mt-4">
<abp-input asp-for="Input.UserName" auto-focus="true" /> @if (!Model.IsExternalLogin)
<abp-input asp-for="Input.EmailAddress" /> {
<abp-input asp-for="Input.Password" /> <abp-input asp-for="Input.UserName" auto-focus="true"/>
<abp-button button-type="Primary" type="submit" class="btn-lg btn-block mt-4">@L["Register"]</abp-button> }
</form>
</div> <abp-input asp-for="Input.EmailAddress"/>
@if (!Model.IsExternalLogin)
{
<abp-input asp-for="Input.Password"/>
}
<abp-button button-type="Primary" type="submit" class="btn-lg btn-block mt-4">@L["Register"]</abp-button>
</form>
</div> </div>
</div>

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

@ -1,14 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.Account.Settings; using Volo.Abp.Account.Settings;
using Volo.Abp.Auditing; using Volo.Abp.Auditing;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.Settings; using Volo.Abp.Settings;
using Volo.Abp.Uow;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using IdentityUser = Volo.Abp.Identity.IdentityUser; using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -27,6 +29,12 @@ namespace Volo.Abp.Account.Web.Pages.Account
[BindProperty] [BindProperty]
public PostInput Input { get; set; } 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) public RegisterModel(IAccountAppService accountAppService)
{ {
AccountAppService = accountAppService; AccountAppService = accountAppService;
@ -35,34 +43,141 @@ namespace Volo.Abp.Account.Web.Pages.Account
public virtual async Task<IActionResult> OnGetAsync() public virtual async Task<IActionResult> OnGetAsync()
{ {
await CheckSelfRegistrationAsync(); await CheckSelfRegistrationAsync();
await TrySetEmailAsync();
return Page(); return Page();
} }
public virtual async Task<IActionResult> 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<IActionResult> OnPostAsync()
{
await CheckSelfRegistrationAsync(); await CheckSelfRegistrationAsync();
var registerDto = new RegisterDto var registerDto = new RegisterDto()
{ {
AppName = "MVC", AppName = "MVC"
EmailAddress = Input.EmailAddress,
Password = Input.Password,
UserName = Input.UserName
}; };
var userDto = await AccountAppService.RegisterAsync(registerDto); if (IsExternalLogin)
var user = await UserManager.GetByIdAsync(userDto.Id); {
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); 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)
{
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() protected virtual async Task CheckSelfRegistrationAsync()
{ {
if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled) || if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled) ||

Loading…
Cancel
Save