From 5d6631328f6ffda0275f8c11fabf9a793afabf9d Mon Sep 17 00:00:00 2001 From: Halil ibrahim Kalkan Date: Mon, 21 Jan 2019 11:35:54 +0300 Subject: [PATCH] Resolved #744: Add IdentityServer4 to the Account Module. --- .../AbpAccountWebModule.cs | 8 +- .../Pages/Account/Login.cshtml | 73 +++-- .../Pages/Account/Login.cshtml.cs | 255 ++++++++++++++++-- .../Volo.Abp.Account.Web.csproj | 1 + 4 files changed, 295 insertions(+), 42 deletions(-) diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs index 13d83b8231..c943ea2d1c 100644 --- a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs +++ b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs @@ -6,6 +6,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; using Volo.Abp.Identity.AspNetCore; +using Volo.Abp.IdentityServer; using Volo.Abp.Localization; using Volo.Abp.Localization.Resources.AbpValidation; using Volo.Abp.Modularity; @@ -15,8 +16,11 @@ using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.Account.Web { - [DependsOn(typeof(AbpIdentityAspNetCoreModule))] - [DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] + [DependsOn( + typeof(AbpIdentityAspNetCoreModule), + typeof(AbpAspNetCoreMvcUiThemeSharedModule), + typeof(AbpIdentityServerDomainModule) + )] public class AbpAccountWebModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) 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 2faa35532b..205f4a5feb 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 @@ -1,30 +1,59 @@ @page -@model Volo.Abp.Account.Web.Pages.Account.LoginModel @using Volo.Abp.Account.Web.Settings -@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage +@model Volo.Abp.Account.Web.Pages.Account.LoginModel @inject Volo.Abp.Settings.ISettingManager SettingManager -

@L["Login"]

+@if (Model.EnableLocalLogin) +{ +
+ + +
+ + + +
+
+ + + +
+
+ +
+ Cancel @* TODO: Only show if identity server is used *@ + Login +
-
- - - - @L["Login"] - @if (string.Equals(await SettingManager.GetOrNullAsync(AccountSettingNames.IsSelfRegistrationEnabled), "true", StringComparison.OrdinalIgnoreCase)) - { - @L["Register"] - } - +
+ @if (string.Equals(await SettingManager.GetOrNullAsync(AccountSettingNames.IsSelfRegistrationEnabled), "true", StringComparison.OrdinalIgnoreCase)) + { + Register | + } +
+} -@if (Model.ExternalLogins.Any()) +@if (Model.VisibleExternalProviders.Any()) { -

Use another service to log in.

-
-
- @foreach (var provider in Model.ExternalLogins) +
+

Use another service to log in.

+ + + + @foreach (var provider in Model.VisibleExternalProviders) { - @provider.DisplayName + } -
- -} \ No newline at end of file + +
+} + +@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) +{ +
+ Invalid login request + There are no login schemes configured for this client. +
+} 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 0507f38912..09dcd7aaef 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 @@ -1,49 +1,181 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using System.Linq; using System.Security.Claims; +using System.Security.Principal; using System.Threading.Tasks; +using IdentityModel; +using IdentityServer4.Events; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Stores; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; +using Volo.Abp.Validation; namespace Volo.Abp.Account.Web.Pages.Account { public class LoginModel : AccountPageModel { + [HiddenInput] [BindProperty(SupportsGet = true)] public string ReturnUrl { get; set; } + [HiddenInput] [BindProperty(SupportsGet = true)] public string ReturnUrlHash { get; set; } [BindProperty] - public PostInput Input { get; set; } + public LoginInputModel LoginInput { get; set; } - public IList ExternalLogins { get; set; } + public bool EnableLocalLogin { get; set; } + + public IList ExternalLogins { get; set; } //TODO: Used? + + //TODO: Why there is an ExternalProviders if only the VisibleExternalProviders is used. + public IEnumerable ExternalProviders { get; set; } + public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); + + public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; + public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + + private readonly IIdentityServerInteractionService _interaction; + private readonly IAuthenticationSchemeProvider _schemeProvider; + private readonly AbpAccountOptions _accountOptions; + private readonly IClientStore _clientStore; + private readonly IEventService _identityServerEvents; + + public LoginModel( + IIdentityServerInteractionService interaction, + IAuthenticationSchemeProvider schemeProvider, + IOptions accountOptions, + IClientStore clientStore, + IEventService identityServerEvents) + { + _interaction = interaction; + _schemeProvider = schemeProvider; + _clientStore = clientStore; + _identityServerEvents = identityServerEvents; + _accountOptions = accountOptions.Value; + } public async Task OnGetAsync() { - ExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + LoginInput = new LoginInputModel(); + + var context = await _interaction.GetAuthorizationContextAsync(ReturnUrl); + + if (context != null) + { + LoginInput.UserNameOrEmailAddress = context.LoginHint; + + //TODO: Reference AspNetCore MultiTenancy module and use options to get the tenant key! + var tenant = context.Parameters[TenantResolverConsts.DefaultTenantKey]; + if (string.IsNullOrEmpty(tenant)) + { + if (Request.Cookies.ContainsKey(TenantResolverConsts.DefaultTenantKey)) + { + CurrentTenant.Change(null); + Response.Cookies.Delete(TenantResolverConsts.DefaultTenantKey); + } + } + else + { + CurrentTenant.Change(Guid.Parse(tenant)); + Response.Cookies.Append(TenantResolverConsts.DefaultTenantKey, tenant); + } + } + + if (context?.IdP != null) + { + LoginInput.UserNameOrEmailAddress = context.LoginHint; + ExternalProviders = new[] { new ExternalProviderModel { AuthenticationScheme = context.IdP } }; + return; + } + + var schemes = await _schemeProvider.GetAllSchemesAsync(); + + var providers = schemes + .Where(x => x.DisplayName != null || x.Name.Equals(_accountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) + .Select(x => new ExternalProviderModel + { + DisplayName = x.DisplayName, + AuthenticationScheme = x.Name + }) + .ToList(); + + EnableLocalLogin = true; //TODO: We can get default from a setting? + if (context?.ClientId != null) + { + var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); + if (client != null) + { + EnableLocalLogin = client.EnableLocalLogin; + + if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) + { + providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); + } + } + } + + ExternalProviders = providers.ToArray(); + + if (IsExternalLoginOnly) + { + //return await ExternalLogin(vm.ExternalLoginScheme, returnUrl); + throw new NotImplementedException(); + } + } [UnitOfWork] //TODO: Will be removed when we implement action filter - public virtual async Task OnPostAsync() + public virtual async Task OnPostAsync(string action) { + EnableLocalLogin = true; //TODO: We can get default from a setting? + + if (action == "Cancel") + { + var context = await _interaction.GetAuthorizationContextAsync(ReturnUrl); + if (context == null) + { + return Redirect("~/"); + } + + await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); + return Redirect(ReturnUrl); + } + ValidateModel(); + await ReplaceEmailToUsernameOfInputIfNeeds(); + var result = await SignInManager.PasswordSignInAsync( - Input.UserNameOrEmailAddress, - Input.Password, - Input.RememberMe, + LoginInput.UserNameOrEmailAddress, + LoginInput.Password, + LoginInput.RememberMe, true ); + if (result.RequiresTwoFactor) + { + return RedirectToPage("./SendSecurityCode", new + { + returnUrl = ReturnUrl, + returnUrlHash = ReturnUrlHash, + rememberMe = LoginInput.RememberMe + }); + } + if (result.IsLockedOut) { Alerts.Warning(L["UserLockedOutMessage"]); @@ -67,22 +199,39 @@ namespace Volo.Abp.Account.Web.Pages.Account return Page(); } + //TODO: Find a way of getting user's id from the logged in user and do not query it again like that! + var user = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress) ?? + await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + + Debug.Assert(user != null, nameof(user) + " != null"); + await _identityServerEvents.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName)); //TODO: Use user's name once implemented + return RedirectSafely(ReturnUrl, ReturnUrlHash); } - - [UnitOfWork] //TODO: Will be removed when we implement action filter - public virtual IActionResult OnPostExternalLogin(string provider, string returnUrl = "", string returnUrlHash = "") + + [UnitOfWork] + public virtual async Task OnPostExternalLogin(string provider) { - var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { returnUrl, returnUrlHash }); + if (_accountOptions.WindowsAuthenticationSchemeName == provider) + { + return await ProcessWindowsLoginAsync(); + } + var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }); var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + properties.Items["scheme"] = provider; - return new ChallengeResult(provider, properties); + return Challenge(properties, provider); } - [UnitOfWork] //TODO: Will be removed when we implement action filter + [UnitOfWork] public virtual async Task OnGetExternalLoginCallbackAsync(string returnUrl = "", string returnUrlHash = "", string remoteError = null) { + //TODO: Did not implemented Identity Server 4 sample for this method (see ExternalLoginCallback in Quickstart of IDS4 sample) + /* Also did not implement these: + * - Logout(string logoutId) + */ + if (remoteError != null) { Logger.LogWarning($"External login callback error: {remoteError}"); @@ -108,13 +257,13 @@ namespace Volo.Abp.Account.Web.Pages.Account throw new UserFriendlyException("Cannot proceed because user is locked out!"); } - //TODO: Handle other cases - if (result.Succeeded) { return RedirectSafely(returnUrl, returnUrlHash); } + //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) @@ -141,18 +290,88 @@ namespace Volo.Abp.Account.Web.Pages.Account return user; } - public class PostInput + private async Task ReplaceEmailToUsernameOfInputIfNeeds() + { + if (!ValidationHandler.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress)) + { + return; + } + + var userByUsername = await UserManager.FindByNameAsync(LoginInput.UserNameOrEmailAddress); + if (userByUsername != null) + { + return; + } + + var userByEmail = await UserManager.FindByEmailAsync(LoginInput.UserNameOrEmailAddress); + if (userByEmail == null) + { + return; + } + + LoginInput.UserNameOrEmailAddress = userByEmail.UserName; + } + + private async Task ProcessWindowsLoginAsync() + { + var result = await HttpContext.AuthenticateAsync(_accountOptions.WindowsAuthenticationSchemeName); + if (!(result?.Principal is WindowsPrincipal windowsPrincipal)) + { + return Challenge(_accountOptions.WindowsAuthenticationSchemeName); + } + + var props = new AuthenticationProperties + { + RedirectUri = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }), + Items = + { + {"scheme", _accountOptions.WindowsAuthenticationSchemeName}, + } + }; + + var identity = new ClaimsIdentity(_accountOptions.WindowsAuthenticationSchemeName); + identity.AddClaim(new Claim(JwtClaimTypes.Subject, windowsPrincipal.Identity.Name)); + identity.AddClaim(new Claim(JwtClaimTypes.Name, windowsPrincipal.Identity.Name)); + + //TODO: Consider to add Windows groups the the identity + //if (_accountOptions.IncludeWindowsGroups) + //{ + // var windowsIdentity = windowsPrincipal.Identity as WindowsIdentity; + // if (windowsIdentity != null) + // { + // var groups = windowsIdentity.Groups?.Translate(typeof(NTAccount)); + // var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value)); + // identity.AddClaims(roles); + // } + //} + + await HttpContext.SignInAsync( + IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme, + new ClaimsPrincipal(identity), + props + ); + + return RedirectSafely(props.RedirectUri); + } + + public class LoginInputModel { [Required] - [StringLength(255)] + [StringLength(IdentityUserConsts.MaxEmailLength)] public string UserNameOrEmailAddress { get; set; } [Required] - [StringLength(32)] + [StringLength(IdentityUserConsts.MaxPasswordLength)] [DataType(DataType.Password)] public string Password { get; set; } public bool RememberMe { get; set; } } + + public class ExternalProviderModel + { + public string DisplayName { get; set; } + public string AuthenticationScheme { get; set; } + } } } diff --git a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj index 50be3cbf0d..29c1f9e6bb 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj +++ b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj @@ -32,6 +32,7 @@ +