From 540eb8f3d88b5b98cc7aee350dfcade7c10cf709 Mon Sep 17 00:00:00 2001 From: Artem Dudarev Date: Fri, 9 Aug 2024 15:40:37 +0200 Subject: [PATCH] VCST-1431: Add external sign in for front end (#2817) --- .../PlatformConstants.cs | 10 + .../AuthenticationPropertiesExtensions.cs | 41 +++++ .../ExternalSignIn/ExternalSignInRequest.cs | 10 + .../ExternalSignIn/ExternalSignInResult.cs | 27 +++ .../ExternalSignIn/IExternalSignInService.cs | 8 + .../IExternalSignInUserBuilder.cs | 9 + .../IExternalSignInValidator.cs | 8 + .../Security/IExternalSigninService.cs | 2 + .../Security/PasswordLoginOptions.cs | 2 +- .../Extensions/ClaimsPrincipalExtensions.cs | 14 +- .../ExternalSignInProviderConfiguration.cs | 0 .../IExternalSignInProvider.cs | 0 .../Api/AuthorizationController.cs | 55 +++++- .../Controllers/Api/SecurityController.cs | 4 +- .../Controllers/ExternalSignInController.cs | 85 ++++++--- ...ninService.cs => ExternalSignInService.cs} | 173 ++++++++---------- src/VirtoCommerce.Platform.Web/Startup.cs | 26 +-- .../en.VirtoCommerce.Platform.json | 4 + 18 files changed, 328 insertions(+), 150 deletions(-) create mode 100644 src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/AuthenticationPropertiesExtensions.cs create mode 100644 src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInRequest.cs create mode 100644 src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInResult.cs create mode 100644 src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInService.cs create mode 100644 src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInUserBuilder.cs create mode 100644 src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInValidator.cs rename src/{VirtoCommerce.Platform.Web/Model => VirtoCommerce.Platform.Core}/Security/PasswordLoginOptions.cs (90%) rename src/VirtoCommerce.Platform.Security/{ExternalSignin => ExternalSignIn}/ExternalSignInProviderConfiguration.cs (100%) rename src/VirtoCommerce.Platform.Security/{ExternalSignin => ExternalSignIn}/IExternalSignInProvider.cs (100%) rename src/VirtoCommerce.Platform.Web/Security/{ExternalSigninService.cs => ExternalSignInService.cs} (55%) diff --git a/src/VirtoCommerce.Platform.Core/PlatformConstants.cs b/src/VirtoCommerce.Platform.Core/PlatformConstants.cs index 5c6fbd45305..6289b980999 100644 --- a/src/VirtoCommerce.Platform.Core/PlatformConstants.cs +++ b/src/VirtoCommerce.Platform.Core/PlatformConstants.cs @@ -14,6 +14,7 @@ public static class Security public static class GrantTypes { public const string Impersonate = "impersonate"; + public const string ExternalSignIn = "external_sign_in"; } public static class Claims @@ -160,6 +161,14 @@ public static class Security DefaultValue = AccountStatuses.DefaultValue, }; + public static SettingDescriptor DefaultExternalAccountStatus { get; } = new() + { + Name = "VirtoCommerce.Platform.Security.DefaultExternalAccountStatus", + GroupName = "Platform|Security", + ValueType = SettingValueType.ShortText, + DefaultValue = "Approved", + }; + public static readonly SettingDescriptor EnablePruneExpiredTokensJob = new SettingDescriptor { Name = "VirtoCommerce.Platform.Security.EnablePruneExpiredTokensJob", @@ -248,6 +257,7 @@ public static IEnumerable AllSettings yield return DefaultAccountType; yield return AccountStatuses; yield return DefaultAccountStatus; + yield return DefaultExternalAccountStatus; yield return EnablePruneExpiredTokensJob; yield return CronPruneExpiredTokensJob; yield return FileExtensionsBlackList; diff --git a/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/AuthenticationPropertiesExtensions.cs b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/AuthenticationPropertiesExtensions.cs new file mode 100644 index 00000000000..77d4fb5e90e --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/AuthenticationPropertiesExtensions.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Authentication; +using VirtoCommerce.Platform.Core.Common; + +namespace VirtoCommerce.Platform.Core.Security.ExternalSignIn; + +public static class AuthenticationPropertiesExtensions +{ + public const string StoreIdPropertyName = "store_id"; + public const string OidcUrlPropertyName = "oidc_url"; + public const string UserTypePropertyName = "user_type"; + + public static void SetStoreId(this AuthenticationProperties properties, string value) + { + properties.Items[StoreIdPropertyName] = value; + } + + public static string GetStoreId(this AuthenticationProperties properties) + { + return properties.Items.GetValueSafe(StoreIdPropertyName); + } + + public static void SetOidcUrl(this AuthenticationProperties properties, string value) + { + properties.Items[OidcUrlPropertyName] = value; + } + + public static string GetOidcUrl(this AuthenticationProperties properties) + { + return properties.Items.GetValueSafe(OidcUrlPropertyName); + } + + public static void SetNewUserType(this AuthenticationProperties properties, string value) + { + properties.Items[UserTypePropertyName] = value; + } + + public static string GetNewUserType(this AuthenticationProperties properties) + { + return properties.Items.GetValueSafe(UserTypePropertyName); + } +} diff --git a/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInRequest.cs b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInRequest.cs new file mode 100644 index 00000000000..8a6f6eba0f9 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInRequest.cs @@ -0,0 +1,10 @@ +namespace VirtoCommerce.Platform.Core.Security.ExternalSignIn; + +public class ExternalSignInRequest +{ + public string AuthenticationType { get; set; } + public string ReturnUrl { get; set; } + public string StoreId { get; set; } + public string OidcUrl { get; set; } + public string CallbackUrl { get; set; } +} diff --git a/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInResult.cs b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInResult.cs new file mode 100644 index 00000000000..8cecd202627 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/ExternalSignInResult.cs @@ -0,0 +1,27 @@ +namespace VirtoCommerce.Platform.Core.Security.ExternalSignIn; + +public class ExternalSignInResult +{ + public bool Success { get; set; } + public string LoginProvider { get; set; } + public ApplicationUser User { get; set; } + + public static ExternalSignInResult Fail() + { + return new ExternalSignInResult + { + Success = false, + User = null, + }; + } + + public static ExternalSignInResult Succeed(string loginProvider, ApplicationUser user) + { + return new ExternalSignInResult + { + Success = true, + LoginProvider = loginProvider, + User = user, + }; + } +} diff --git a/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInService.cs b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInService.cs new file mode 100644 index 00000000000..dee3b2f9370 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace VirtoCommerce.Platform.Core.Security.ExternalSignIn; + +public interface IExternalSignInService +{ + public Task SignInAsync(); +} diff --git a/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInUserBuilder.cs b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInUserBuilder.cs new file mode 100644 index 00000000000..d5e178ad714 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInUserBuilder.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace VirtoCommerce.Platform.Core.Security.ExternalSignIn; + +public interface IExternalSignInUserBuilder +{ + public Task BuildNewUser(ApplicationUser user, ExternalLoginInfo externalLoginInfo); +} diff --git a/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInValidator.cs b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInValidator.cs new file mode 100644 index 00000000000..39429470e83 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Security/ExternalSignIn/IExternalSignInValidator.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace VirtoCommerce.Platform.Core.Security.ExternalSignIn; + +public interface IExternalSignInValidator +{ + Task ValidateAsync(ExternalSignInRequest request); +} diff --git a/src/VirtoCommerce.Platform.Core/Security/IExternalSigninService.cs b/src/VirtoCommerce.Platform.Core/Security/IExternalSigninService.cs index 2e36a2bd42f..269d2ee014b 100644 --- a/src/VirtoCommerce.Platform.Core/Security/IExternalSigninService.cs +++ b/src/VirtoCommerce.Platform.Core/Security/IExternalSigninService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -5,6 +6,7 @@ namespace VirtoCommerce.Platform.Core.Security { public interface IExternalSigninService { + [Obsolete("Not being called. Use IExternalSignInService.SignInAsync()", DiagnosticId = "VC0009", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions/")] public Task ProcessCallbackAsync(string returnUrl, IUrlHelper urlHelper); } } diff --git a/src/VirtoCommerce.Platform.Web/Model/Security/PasswordLoginOptions.cs b/src/VirtoCommerce.Platform.Core/Security/PasswordLoginOptions.cs similarity index 90% rename from src/VirtoCommerce.Platform.Web/Model/Security/PasswordLoginOptions.cs rename to src/VirtoCommerce.Platform.Core/Security/PasswordLoginOptions.cs index ead7dd047f4..449222b4cae 100644 --- a/src/VirtoCommerce.Platform.Web/Model/Security/PasswordLoginOptions.cs +++ b/src/VirtoCommerce.Platform.Core/Security/PasswordLoginOptions.cs @@ -1,4 +1,4 @@ -namespace VirtoCommerce.Platform.Web.Model.Security +namespace VirtoCommerce.Platform.Core.Security { public class PasswordLoginOptions { diff --git a/src/VirtoCommerce.Platform.Security/Extensions/ClaimsPrincipalExtensions.cs b/src/VirtoCommerce.Platform.Security/Extensions/ClaimsPrincipalExtensions.cs index aebfbcfb077..dbcd511187f 100644 --- a/src/VirtoCommerce.Platform.Security/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/VirtoCommerce.Platform.Security/Extensions/ClaimsPrincipalExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Security.Claims; using OpenIddict.Abstractions; @@ -6,14 +7,25 @@ namespace VirtoCommerce.Platform.Security.Extensions { public static class ClaimsPrincipalExtensions { + public static bool IsExternalSignIn(this ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal?.GetAuthenticationMethod() != null; + } + public static string GetAuthenticationMethod(this ClaimsPrincipal claimsPrincipal) { return claimsPrincipal?.GetClaim(ClaimTypes.AuthenticationMethod); } + public static ClaimsPrincipal SetAuthenticationMethod(this ClaimsPrincipal claimsPrincipal, string value, IList destinations) + { + return claimsPrincipal?.SetClaimWithDestinations(ClaimTypes.AuthenticationMethod, value, destinations); + } + + [Obsolete("Use IsExternalSignIn()", DiagnosticId = "VC0009", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions/")] public static bool IsSsoAuthenticationMethod(this ClaimsPrincipal claimsPrincipal) { - return claimsPrincipal?.GetClaim(ClaimTypes.AuthenticationMethod) != null; + return claimsPrincipal.IsExternalSignIn(); } public static ClaimsPrincipal SetClaimWithDestinations(this ClaimsPrincipal claimsPrincipal, string type, string value, IList destinations) diff --git a/src/VirtoCommerce.Platform.Security/ExternalSignin/ExternalSignInProviderConfiguration.cs b/src/VirtoCommerce.Platform.Security/ExternalSignIn/ExternalSignInProviderConfiguration.cs similarity index 100% rename from src/VirtoCommerce.Platform.Security/ExternalSignin/ExternalSignInProviderConfiguration.cs rename to src/VirtoCommerce.Platform.Security/ExternalSignIn/ExternalSignInProviderConfiguration.cs diff --git a/src/VirtoCommerce.Platform.Security/ExternalSignin/IExternalSignInProvider.cs b/src/VirtoCommerce.Platform.Security/ExternalSignIn/IExternalSignInProvider.cs similarity index 100% rename from src/VirtoCommerce.Platform.Security/ExternalSignin/IExternalSignInProvider.cs rename to src/VirtoCommerce.Platform.Security/ExternalSignIn/IExternalSignInProvider.cs diff --git a/src/VirtoCommerce.Platform.Web/Controllers/Api/AuthorizationController.cs b/src/VirtoCommerce.Platform.Web/Controllers/Api/AuthorizationController.cs index 85a242d86c8..4f0d5dbfd9b 100644 --- a/src/VirtoCommerce.Platform.Web/Controllers/Api/AuthorizationController.cs +++ b/src/VirtoCommerce.Platform.Web/Controllers/Api/AuthorizationController.cs @@ -19,11 +19,11 @@ using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Security.Events; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; using VirtoCommerce.Platform.Security.Authorization; using VirtoCommerce.Platform.Security.Extensions; using VirtoCommerce.Platform.Security.OpenIddict; using VirtoCommerce.Platform.Web.Extensions; -using VirtoCommerce.Platform.Web.Model.Security; using static OpenIddict.Abstractions.OpenIddictConstants; namespace VirtoCommerce.Platform.Web.Controllers.Api @@ -40,31 +40,31 @@ public class AuthorizationController : Controller private readonly IEnumerable _claimProviders; private readonly OpenIddictTokenManager _tokenManager; private readonly IAuthorizationService _authorizationService; - - private UserManager UserManager => _signInManager.UserManager; + private readonly IExternalSignInService _externalSignInService; public AuthorizationController( OpenIddictApplicationManager applicationManager, IOptions identityOptions, SignInManager signInManager, - UserManager userManager, IOptions passwordLoginOptions, IEventPublisher eventPublisher, IEnumerable requestValidators, IEnumerable claimProviders, OpenIddictTokenManager tokenManager, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + IExternalSignInService externalSignInService) { _applicationManager = applicationManager; _identityOptions = identityOptions.Value; _passwordLoginOptions = passwordLoginOptions.Value; _signInManager = signInManager; - _userManager = userManager; + _userManager = _signInManager.UserManager; _eventPublisher = eventPublisher; _requestValidators = requestValidators.OrderByDescending(x => x.Priority).ThenBy(x => x.GetType().Name).ToList(); _claimProviders = claimProviders; _tokenManager = tokenManager; _authorizationService = authorizationService; + _externalSignInService = externalSignInService; } [HttpPost("~/revoke/token")] @@ -122,7 +122,7 @@ public async Task Exchange() // Allows signin to back office by either username (login) or email if IdentityOptions.User.RequireUniqueEmail is True. if (user is null && _identityOptions.User.RequireUniqueEmail) { - user = await UserManager.FindByEmailAsync(openIdConnectRequest.Username); + user = await _userManager.FindByEmailAsync(openIdConnectRequest.Username); } if (user is null) @@ -196,6 +196,41 @@ public async Task Exchange() // Create a new authentication ticket, but reuse the properties stored in the // authorization code/refresh token, including the scopes originally granted. var ticket = await CreateTicketAsync(user, context); + ticket.Principal.SetAuthenticationMethod(info.Principal.GetAuthenticationMethod(), [Destinations.AccessToken]); + + return SignIn(ticket.Principal, ticket.AuthenticationScheme); + } + + if (openIdConnectRequest.GrantType == PlatformConstants.Security.GrantTypes.ExternalSignIn) + { + var signInResult = await _externalSignInService.SignInAsync(); + + // Remove identity cookies regardless of the result + await _signInManager.SignOutAsync(); + + if (!signInResult.Success) + { + return BadRequest(SecurityErrorDescriber.LoginFailed()); + } + + if (!await _signInManager.CanSignInAsync(signInResult.User)) + { + return BadRequest(SecurityErrorDescriber.SignInNotAllowed()); + } + + context.User = signInResult.User.CloneTyped(); + + foreach (var requestValidator in _requestValidators) + { + var errors = await requestValidator.ValidateAsync(context); + if (errors.Count > 0) + { + return BadRequest(errors.First()); + } + } + + var ticket = await CreateTicketAsync(signInResult.User, context); + ticket.Principal.SetAuthenticationMethod(signInResult.LoginProvider, [Destinations.AccessToken]); return SignIn(ticket.Principal, ticket.AuthenticationScheme); } @@ -248,12 +283,12 @@ public async Task Exchange() if (!string.IsNullOrEmpty(userId)) { // Find impersonated user by id - impersonatedUser = await _signInManager.UserManager.FindByIdAsync(userId); + impersonatedUser = await _userManager.FindByIdAsync(userId); } else { // Reset impersonation to operator - impersonatedUser = await _signInManager.UserManager.FindByIdAsync(operatorUserId); + impersonatedUser = await _userManager.FindByIdAsync(operatorUserId); operatorUserId = string.Empty; operatorUserName = string.Empty; } @@ -388,7 +423,7 @@ private async Task CreateTicketAsync(ApplicationUser user, private Task SetLastLoginDate(ApplicationUser user) { user.LastLoginDate = DateTime.UtcNow; - return _signInManager.UserManager.UpdateAsync(user); + return _userManager.UpdateAsync(user); } } } diff --git a/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs b/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs index 2c1238012f3..2d3275320e0 100644 --- a/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs +++ b/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs @@ -177,7 +177,7 @@ public async Task> GetCurrentUser() DaysTillPasswordExpiry = PasswordExpiryHelper.ContDaysTillPasswordExpiry(user, _userOptionsExtended), Permissions = user.Roles.SelectMany(x => x.Permissions).Select(x => x.Name).Distinct().ToArray(), AuthenticationMethod = HttpContext.User.GetAuthenticationMethod(), - IsSsoAuthenticationMethod = HttpContext.User.IsSsoAuthenticationMethod() + IsSsoAuthenticationMethod = HttpContext.User.IsExternalSignIn(), }; // Password never expired with SSO @@ -439,7 +439,7 @@ public async Task> Create([FromBody] ApplicationUse [Authorize] public async Task> ChangeCurrentUserPassword([FromBody] ChangePasswordRequest changePassword) { - if (HttpContext.User.IsSsoAuthenticationMethod()) + if (HttpContext.User.IsExternalSignIn()) { return BadRequest(new SecurityResult { Errors = new[] { $"Could not change password for {HttpContext.User.GetAuthenticationMethod()} authentication method" } }); } diff --git a/src/VirtoCommerce.Platform.Web/Controllers/ExternalSignInController.cs b/src/VirtoCommerce.Platform.Web/Controllers/ExternalSignInController.cs index f5db926242c..8864ed880e2 100644 --- a/src/VirtoCommerce.Platform.Web/Controllers/ExternalSignInController.cs +++ b/src/VirtoCommerce.Platform.Web/Controllers/ExternalSignInController.cs @@ -9,6 +9,7 @@ using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Security.Events; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; using VirtoCommerce.Platform.Security.ExternalSignIn; using VirtoCommerce.Platform.Web.Model.Security; @@ -18,41 +19,70 @@ namespace VirtoCommerce.Platform.Web.Controllers public class ExternalSignInController : Controller { private readonly SignInManager _signInManager; - private readonly IExternalSigninService _externalSigninService; + private readonly UserManager _userManager; + private readonly IExternalSignInService _externalSignInService; private readonly IEventPublisher _eventPublisher; private readonly IEnumerable _externalSigninProviderConfigs; + private readonly IList _externalSignInValidators; public ExternalSignInController(SignInManager signInManager, - IExternalSigninService externalSigninService, + IExternalSignInService externalSignInService, IEventPublisher eventPublisher, - IEnumerable externalSigninProviderConfigs) + IEnumerable externalSigninProviderConfigs, + IEnumerable externalSignInValidators) { _signInManager = signInManager; - _externalSigninService = externalSigninService; + _userManager = _signInManager.UserManager; + _externalSignInService = externalSignInService; _eventPublisher = eventPublisher; _externalSigninProviderConfigs = externalSigninProviderConfigs; + _externalSignInValidators = externalSignInValidators.ToList(); } [HttpGet] [Route("")] [AllowAnonymous] - public ActionResult SignIn(string authenticationType, string returnUrl = null) + public async Task SignIn([FromQuery] ExternalSignInRequest request) { - if (string.IsNullOrEmpty(authenticationType)) + if (string.IsNullOrEmpty(request.AuthenticationType)) { return BadRequest(); } - if (string.IsNullOrEmpty(returnUrl)) + if (string.IsNullOrEmpty(request.ReturnUrl)) { - returnUrl = Url.Action("Index", "Home"); + request.ReturnUrl = GetHomeUrl(); } - var callbackUrl = Url.Action("SignInCallback", "ExternalSignIn", new { returnUrl }); - var authenticationProperties = new AuthenticationProperties { RedirectUri = callbackUrl }; - authenticationProperties.Items["LoginProvider"] = authenticationType; + var authenticationProperties = new AuthenticationProperties + { + Items = { ["LoginProvider"] = request.AuthenticationType }, + RedirectUri = Url.Action("SignInCallback", "ExternalSignIn", new { request.ReturnUrl }), + }; + + // Validate and apply front-end parameters + if (!string.IsNullOrEmpty(request.StoreId) && !string.IsNullOrEmpty(request.OidcUrl) && !string.IsNullOrEmpty(request.CallbackUrl)) + { + if (_externalSignInValidators.Count == 0) + { + return BadRequest(); + } + + foreach (var validator in _externalSignInValidators) + { + if (!await validator.ValidateAsync(request)) + { + return BadRequest(); + } + } + + authenticationProperties.SetStoreId(request.StoreId); + authenticationProperties.SetOidcUrl(request.OidcUrl); + authenticationProperties.SetNewUserType(UserType.Customer.ToString()); + authenticationProperties.RedirectUri = request.CallbackUrl; + } - return Challenge(authenticationProperties, authenticationType); + return Challenge(authenticationProperties, request.AuthenticationType); } [HttpGet] @@ -64,12 +94,12 @@ public async Task SignOut(string authenticationType) return BadRequest(); } - var userName = User?.Identity?.Name; + var userName = User.Identity?.Name; // sign out the current user if (!string.IsNullOrEmpty(userName)) { - var user = await _signInManager.UserManager.FindByNameAsync(User?.Identity?.Name); + var user = await _userManager.FindByNameAsync(userName); if (user != null) { await _signInManager.SignOutAsync(); @@ -87,7 +117,11 @@ public async Task SignOut(string authenticationType) [AllowAnonymous] public async Task SignInCallback(string returnUrl) { - var redirectUrl = await _externalSigninService.ProcessCallbackAsync(returnUrl, Url); + var signInResult = await _externalSignInService.SignInAsync(); + + var redirectUrl = signInResult.Success && Url.IsLocalUrl(returnUrl) + ? returnUrl + : GetHomeUrl(); return Redirect(redirectUrl); } @@ -97,18 +131,23 @@ public async Task SignInCallback(string returnUrl) [AllowAnonymous] public async Task> GetExternalLoginProviders() { - var externalLoginProviders = (await _signInManager.GetExternalAuthenticationSchemesAsync()) - .Select(authenticationDescription => new ExternalSignInProviderInfo + var providers = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Select(scheme => new ExternalSignInProviderInfo { - AuthenticationType = authenticationDescription.Name, - DisplayName = authenticationDescription.DisplayName, - LogoUrl = _externalSigninProviderConfigs? - .FirstOrDefault(x => x.AuthenticationType.EqualsInvariant(authenticationDescription.Name))? - .LogoUrl, + AuthenticationType = scheme.Name, + DisplayName = scheme.DisplayName, + LogoUrl = _externalSigninProviderConfigs + ?.FirstOrDefault(x => x.AuthenticationType.EqualsIgnoreCase(scheme.Name)) + ?.LogoUrl, }) .ToArray(); - return Ok(externalLoginProviders); + return Ok(providers); + } + + private string GetHomeUrl() + { + return Url.Action("Index", "Home") ?? "/"; } } } diff --git a/src/VirtoCommerce.Platform.Web/Security/ExternalSigninService.cs b/src/VirtoCommerce.Platform.Web/Security/ExternalSignInService.cs similarity index 55% rename from src/VirtoCommerce.Platform.Web/Security/ExternalSigninService.cs rename to src/VirtoCommerce.Platform.Web/Security/ExternalSignInService.cs index 30f9a46408d..e803617dde9 100644 --- a/src/VirtoCommerce.Platform.Web/Security/ExternalSigninService.cs +++ b/src/VirtoCommerce.Platform.Web/Security/ExternalSignInService.cs @@ -6,19 +6,20 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using VirtoCommerce.Platform.Core; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.Platform.Core.Security; using VirtoCommerce.Platform.Core.Security.Events; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; using VirtoCommerce.Platform.Core.Settings; using VirtoCommerce.Platform.Security.ExternalSignIn; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; namespace VirtoCommerce.Platform.Web.Security { - public class ExternalSigninService : IExternalSigninService + public class ExternalSignInService : IExternalSignInService, IExternalSigninService { private readonly SignInManager _signInManager; private readonly UserManager _userManager; @@ -26,39 +27,46 @@ public class ExternalSigninService : IExternalSigninService private readonly IdentityOptions _identityOptions; private readonly ISettingsManager _settingsManager; private readonly IEnumerable _externalSigninProviderConfigs; + private readonly IEnumerable _userBuilders; - private IUrlHelper _urlHelper; - - [ActivatorUtilitiesConstructor] - public ExternalSigninService(SignInManager signInManager, - UserManager userManager, + public ExternalSignInService( + SignInManager signInManager, IEventPublisher eventPublisher, IOptions identityOptions, ISettingsManager settingsManager, - IEnumerable externalSigninProviderConfigs) + IEnumerable externalSigninProviderConfigs, + IEnumerable userBuilders) { _signInManager = signInManager; - _userManager = userManager; + _userManager = signInManager.UserManager; _eventPublisher = eventPublisher; _identityOptions = identityOptions.Value; _settingsManager = settingsManager; _externalSigninProviderConfigs = externalSigninProviderConfigs; + _userBuilders = userBuilders; } + [Obsolete("Not being called. Use SignInAsync()", DiagnosticId = "VC0009", UrlFormat = "https://docs.virtocommerce.org/products/products-virto3-versions/")] public virtual async Task ProcessCallbackAsync(string returnUrl, IUrlHelper urlHelper) { - _urlHelper = urlHelper; + var signInResult = await SignInAsync(); - if (!_urlHelper.IsLocalUrl(returnUrl)) - { - return _urlHelper.Action("index", "Home"); - } + return signInResult.Success && urlHelper.IsLocalUrl(returnUrl) + ? returnUrl + : urlHelper.Action("Index", "Home") ?? "/"; + } + public virtual async Task SignInAsync() + { var externalLoginInfo = await _signInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo is null) + { + return ExternalSignInResult.Fail(); + } - if (!TryGetUserInfo(externalLoginInfo, out var userName, out var userEmail, out var redirectUrl)) + if (!TryGetUserInfo(externalLoginInfo, out var userName, out var userEmail)) { - return redirectUrl; + return ExternalSignInResult.Fail(); } var platformUser = await GetOrCreatePlatformUser(externalLoginInfo, userName, userEmail); @@ -69,119 +77,107 @@ public virtual async Task ProcessCallbackAsync(string returnUrl, IUrlHel await _eventPublisher.Publish(new BeforeUserLoginEvent(platformUser, externalLoginInfo)); - var externalLoginResult = await _signInManager.ExternalLoginSignInAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, false); + var externalLoginResult = await _signInManager.ExternalLoginSignInAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, isPersistent: false); if (externalLoginResult == SignInResult.Failed) { throw new AuthenticationException($"The requested provider {externalLoginInfo.ProviderDisplayName} has not been linked to an account, the provider must be linked from the back office."); } - else if (externalLoginResult == SignInResult.LockedOut) + + if (externalLoginResult == SignInResult.LockedOut) { throw new AuthenticationException($"The user {externalLoginInfo.Principal.Identity?.Name} for the external provider {externalLoginInfo.ProviderDisplayName} is locked out."); } - else if (externalLoginResult == SignInResult.TwoFactorRequired) - { - throw new NotImplementedException(); - } - var validationResult = await ValidateUserAsync(platformUser, externalLoginResult, returnUrl); - if (!validationResult.Item1) + if (externalLoginResult == SignInResult.TwoFactorRequired) { - return validationResult.Item2; + throw new NotImplementedException(); } await SetLastLoginDate(platformUser); await _eventPublisher.Publish(new UserLoginEvent(platformUser, externalLoginInfo)); - return returnUrl; + return ExternalSignInResult.Succeed(externalLoginInfo.LoginProvider, platformUser); } - private Task SetLastLoginDate(ApplicationUser user) + private Task SetLastLoginDate(ApplicationUser user) { user.LastLoginDate = DateTime.UtcNow; - return _signInManager.UserManager.UpdateAsync(user); - } - - protected virtual Task<(bool, string)> ValidateUserAsync(ApplicationUser platformUser, SignInResult externalLoginResult, string returnUrl) - { - return Task.FromResult((true, returnUrl)); + return _userManager.UpdateAsync(user); } - protected virtual async Task GetOrCreatePlatformUser(ExternalLoginInfo externalLoginInfo, string userName, string userEmail) + private async Task GetOrCreatePlatformUser(ExternalLoginInfo externalLoginInfo, string userName, string userEmail) { //Need handle the two cases //first - when the VC platform user account already exists, it is just missing an external login info and //second - when user does not have an account, then create a new account for them - var platformUser = await _userManager.FindByNameAsync(userName); + var user = await _userManager.FindByNameAsync(userName); - if (_identityOptions.User.RequireUniqueEmail && platformUser == null) + if (user == null && _identityOptions.User.RequireUniqueEmail && !string.IsNullOrEmpty(userEmail)) { - platformUser = await FindUserByEmail(userEmail); + user = await _userManager.FindByEmailAsync(userEmail); } - var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo); - if (platformUser == null) + if (user == null && AllowCreateNewUser(externalLoginInfo)) { - if (providerConfig?.Provider.AllowCreateNewUser == true) + user = AbstractTypeFactory.TryCreateInstance(); + user.UserName = userName; + user.Email = userEmail; + user.EmailConfirmed = true; + user.UserType = await GetDefaultUserType(externalLoginInfo); + user.StoreId = externalLoginInfo.AuthenticationProperties.GetStoreId(); + user.Status = await _settingsManager.GetValueAsync(PlatformConstants.Settings.Security.DefaultExternalAccountStatus); + + foreach (var userBuilder in _userBuilders) + { + await userBuilder.BuildNewUser(user, externalLoginInfo); + } + + var result = await _userManager.CreateAsync(user); + if (!result.Succeeded) + { + var joinedErrors = string.Join(Environment.NewLine, result.Errors.Select(x => x.Description)); + throw new InvalidOperationException("Failed to save a VC platform account due the errors: " + joinedErrors); + } + + var roles = GetDefaultUserRoles(externalLoginInfo); + + if (roles is { Length: > 0 }) { - platformUser = new ApplicationUser - { - UserName = userName, - Email = userEmail, - UserType = await GetDefaultUserType(externalLoginInfo) - }; - - var result = await _userManager.CreateAsync(platformUser); - if (!result.Succeeded) - { - var joinedErrors = string.Join(Environment.NewLine, result.Errors.Select(x => x.Description)); - throw new InvalidOperationException("Failed to save a VC platform account due the errors: " + joinedErrors); - } - - var roles = GetDefaultUserRoles(externalLoginInfo); - - if (roles is { Length: > 0 }) - { - await _userManager.AddToRolesAsync(platformUser, roles); - } + await _userManager.AddToRolesAsync(user, roles); } } - var user = await _userManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey); - if (user == null) + if (user != null && await _userManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey) == null) { // Register a new external login var newExternalLogin = new UserLoginInfo(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, externalLoginInfo.ProviderDisplayName); - await _userManager.AddLoginAsync(platformUser, newExternalLogin); + await _userManager.AddLoginAsync(user, newExternalLogin); } - - return platformUser; + return user; } - protected virtual async Task FindUserByEmail(string email) + private bool AllowCreateNewUser(ExternalLoginInfo externalLoginInfo) { - if (string.IsNullOrWhiteSpace(email)) - { - return null; - } - - return await _userManager.FindByEmailAsync(email); + var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo); + return providerConfig?.Provider.AllowCreateNewUser == true; } - - protected virtual async Task GetDefaultUserType(ExternalLoginInfo externalLoginInfo) + private async Task GetDefaultUserType(ExternalLoginInfo externalLoginInfo) { - var userType = "Manager"; + var userType = externalLoginInfo.AuthenticationProperties?.GetNewUserType(); - var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo); - if (providerConfig?.Provider is not null) + if (string.IsNullOrEmpty(userType)) { - userType = providerConfig.Provider.GetUserType(); - } + var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo); - var userTypesSetting = await _settingsManager.GetObjectSettingAsync("VirtoCommerce.Platform.Security.AccountTypes"); + userType = providerConfig?.Provider is not null + ? providerConfig.Provider.GetUserType() + : "Manager"; + } + var userTypesSetting = await _settingsManager.GetObjectSettingAsync(PlatformConstants.Settings.Security.SecurityAccountTypes.Name); var userTypes = userTypesSetting.AllowedValues.Select(x => x.ToString()).ToList(); if (!userTypes.Contains(userType)) @@ -191,14 +187,14 @@ protected virtual async Task GetDefaultUserType(ExternalLoginInfo extern using (await AsyncLock.GetLockByKey("settings").GetReleaserAsync()) { - await _settingsManager.SaveObjectSettingsAsync(new[] { userTypesSetting }); + await _settingsManager.SaveObjectSettingsAsync([userTypesSetting]); } } return userType; } - protected virtual string[] GetDefaultUserRoles(ExternalLoginInfo externalLoginInfo) + private string[] GetDefaultUserRoles(ExternalLoginInfo externalLoginInfo) { var userRoles = Array.Empty(); var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo); @@ -210,17 +206,10 @@ protected virtual string[] GetDefaultUserRoles(ExternalLoginInfo externalLoginIn return userRoles; } - protected virtual bool TryGetUserInfo(ExternalLoginInfo externalLoginInfo, out string userName, out string userEmail, out string redirectUrl) + private bool TryGetUserInfo(ExternalLoginInfo externalLoginInfo, out string userName, out string userEmail) { userName = string.Empty; userEmail = string.Empty; - redirectUrl = string.Empty; - - if (externalLoginInfo == null) - { - redirectUrl = _urlHelper.Action("index", "Home"); - return false; - } var providerConfig = GetExternalSigninProviderConfiguration(externalLoginInfo); if (providerConfig?.Provider is not null) @@ -238,9 +227,9 @@ protected virtual bool TryGetUserInfo(ExternalLoginInfo externalLoginInfo, out s return true; } - protected virtual ExternalSignInProviderConfiguration GetExternalSigninProviderConfiguration(ExternalLoginInfo externalLoginInfo) + private ExternalSignInProviderConfiguration GetExternalSigninProviderConfiguration(ExternalLoginInfo externalLoginInfo) { - return _externalSigninProviderConfigs.FirstOrDefault(x => x.AuthenticationType.EqualsInvariant(externalLoginInfo.LoginProvider)); + return _externalSigninProviderConfigs.FirstOrDefault(x => x.AuthenticationType.EqualsIgnoreCase(externalLoginInfo.LoginProvider)); } } } diff --git a/src/VirtoCommerce.Platform.Web/Startup.cs b/src/VirtoCommerce.Platform.Web/Startup.cs index f01993914fa..fa843f76b35 100644 --- a/src/VirtoCommerce.Platform.Web/Startup.cs +++ b/src/VirtoCommerce.Platform.Web/Startup.cs @@ -37,12 +37,12 @@ using VirtoCommerce.Platform.Core; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.DynamicProperties; -using VirtoCommerce.Platform.Core.Events; using VirtoCommerce.Platform.Core.JsonConverters; using VirtoCommerce.Platform.Core.Localizations; using VirtoCommerce.Platform.Core.Logger; using VirtoCommerce.Platform.Core.Modularity; using VirtoCommerce.Platform.Core.Security; +using VirtoCommerce.Platform.Core.Security.ExternalSignIn; using VirtoCommerce.Platform.Core.Settings; using VirtoCommerce.Platform.Data.Extensions; using VirtoCommerce.Platform.Data.MySql; @@ -58,7 +58,6 @@ using VirtoCommerce.Platform.Modules.Local; using VirtoCommerce.Platform.Security; using VirtoCommerce.Platform.Security.Authorization; -using VirtoCommerce.Platform.Security.ExternalSignIn; using VirtoCommerce.Platform.Security.Repositories; using VirtoCommerce.Platform.Security.Services; using VirtoCommerce.Platform.Web.Extensions; @@ -68,7 +67,6 @@ using VirtoCommerce.Platform.Web.Licensing; using VirtoCommerce.Platform.Web.Middleware; using VirtoCommerce.Platform.Web.Migrations; -using VirtoCommerce.Platform.Web.Model.Security; using VirtoCommerce.Platform.Web.PushNotifications; using VirtoCommerce.Platform.Web.Redis; using VirtoCommerce.Platform.Web.Security; @@ -365,7 +363,8 @@ public void ConfigureServices(IServiceCollection services) options.AllowPasswordFlow() .AllowRefreshTokenFlow() .AllowClientCredentialsFlow() - .AllowCustomFlow(PlatformConstants.Security.GrantTypes.Impersonate); + .AllowCustomFlow(PlatformConstants.Security.GrantTypes.Impersonate) + .AllowCustomFlow(PlatformConstants.Security.GrantTypes.ExternalSignIn); options.SetRefreshTokenLifetime(authorizationOptions?.RefreshTokenLifeTime); options.SetAccessTokenLifetime(authorizationOptions?.AccessTokenLifeTime); @@ -460,23 +459,8 @@ public void ConfigureServices(IServiceCollection services) //Platform authorization handler for policies based on permissions services.AddSingleton(); - // register ExternalSigninService using non-obsolete constructor - services.AddTransient(provider => - { - var signInManager = provider.GetRequiredService>(); - var userManager = provider.GetRequiredService>(); - var eventPublisher = provider.GetRequiredService(); - var identityOptions = provider.GetRequiredService>(); - var settingsManager = provider.GetRequiredService(); - var externalSigninProviderConfigs = provider.GetRequiredService>(); - - return new ExternalSigninService(signInManager, - userManager, - eventPublisher, - identityOptions, - settingsManager, - externalSigninProviderConfigs); - }); + services.AddTransient(); + services.AddTransient(); services.AddOptions().Bind(Configuration.GetSection("VirtoCommerce")) .PostConfigure(options => diff --git a/src/VirtoCommerce.Platform.Web/wwwroot/Localizations/en.VirtoCommerce.Platform.json b/src/VirtoCommerce.Platform.Web/wwwroot/Localizations/en.VirtoCommerce.Platform.json index ca56da1e4e0..3680d143c04 100644 --- a/src/VirtoCommerce.Platform.Web/wwwroot/Localizations/en.VirtoCommerce.Platform.json +++ b/src/VirtoCommerce.Platform.Web/wwwroot/Localizations/en.VirtoCommerce.Platform.json @@ -954,6 +954,10 @@ "title": "Default account status", "description": "Default value for account status when creating a new account" }, + "DefaultExternalAccountStatus": { + "title": "Default external account status", + "description": "Default value for account status when creating a new account during external sign in" + }, "EnablePruneExpiredTokensJob": { "description": "Enables prune expired and invalid tokens/authorizations", "title": "Prune expired tokens"