diff --git a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs index 2cb6fd3b4..f83b15e2a 100644 --- a/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Authentication/Services/ExternalAuthenticationService.cs @@ -13,7 +13,6 @@ using Grand.Infrastructure.Extensions; using Grand.SharedKernel.Extensions; using Grand.Mediator; -using Microsoft.AspNetCore.Mvc; namespace Grand.Business.Authentication.Services; @@ -25,7 +24,6 @@ public class ExternalAuthenticationService : IExternalAuthenticationService #region Ctor public ExternalAuthenticationService( - IGrandAuthenticationService authenticationService, ICustomerManagerService customerManagerService, ICustomerService customerService, IGroupService groupService, @@ -37,7 +35,6 @@ public ExternalAuthenticationService( { _customerSettings = customerSettings; _externalAuthenticationSettings = externalAuthenticationSettings; - _authenticationService = authenticationService; _customerManagerService = customerManagerService; _customerService = customerService; _groupService = groupService; @@ -50,7 +47,6 @@ public ExternalAuthenticationService( #region Fields - private readonly IGrandAuthenticationService _authenticationService; private readonly ICustomerManagerService _customerManagerService; private readonly ICustomerService _customerService; private readonly IGroupService _groupService; @@ -72,22 +68,19 @@ public ExternalAuthenticationService( /// Customer of the request (a guest when nobody is logged in) /// Store identifier /// Working language of the request - /// URL to which the user will return after authentication /// Result of an authentication - protected virtual async Task AuthenticateExistingUser(Customer associatedUser, - Customer currentLoggedInUser, Customer customer, string storeId, Language language, string returnUrl) + protected virtual async Task AuthenticateExistingUser(Customer associatedUser, + Customer currentLoggedInUser, Customer customer, string storeId, Language language) { //log in guest user if (currentLoggedInUser == null) - return await LoginUser(associatedUser, customer, storeId, language, returnUrl); + return await LoginUser(associatedUser, customer, storeId, language); //account is already assigned to another user if (currentLoggedInUser.Id != associatedUser.Id) - return Error(["Account is already assigned"]); + return ExternalAuthResult.AlreadyAssigned(); - if (string.IsNullOrEmpty(returnUrl)) - return new RedirectToRouteResult("HomePage", new { area = "" }); - return new RedirectResult(returnUrl); + return ExternalAuthResult.Associated(currentLoggedInUser); } /// @@ -98,26 +91,23 @@ protected virtual async Task AuthenticateExistingUser(Customer as /// Customer of the request (a guest when nobody is logged in) /// Store identifier /// Working language of the request - /// URL to which the user will return after authentication /// Result of an authentication - protected virtual async Task AuthenticateNewUser(Customer currentLoggedInUser, - ExternalAuthParam parameters, Customer customer, string storeId, Language language, string returnUrl) + protected virtual async Task AuthenticateNewUser(Customer currentLoggedInUser, + ExternalAuthParam parameters, Customer customer, string storeId, Language language) { //associate external account with logged-in user if (currentLoggedInUser != null) { await AssociateCustomer(currentLoggedInUser, parameters); - if (string.IsNullOrEmpty(returnUrl)) - return new RedirectToRouteResult("HomePage", new { area = "" }); - return new RedirectResult(returnUrl); + return ExternalAuthResult.Associated(currentLoggedInUser); } //or try to register new user if (_customerSettings.UserRegistrationType != UserRegistrationType.Disabled) - return await RegisterNewUser(parameters, customer, storeId, language, returnUrl); + return await RegisterNewUser(parameters, customer, storeId, language); //registration is disabled - return Error(["Registration is disabled"]); + return ExternalAuthResult.RegistrationDisabled(); } /// @@ -127,10 +117,9 @@ protected virtual async Task AuthenticateNewUser(Customer current /// Guest customer of the request; registration turns it into the new user /// Store identifier /// Working language of the request - /// URL to which the user will return after authentication /// Result of an authentication - protected virtual async Task RegisterNewUser(ExternalAuthParam parameters, Customer customer, - string storeId, Language language, string returnUrl) + protected virtual async Task RegisterNewUser(ExternalAuthParam parameters, Customer customer, + string storeId, Language language) { var approved = _customerSettings.UserRegistrationType is UserRegistrationType.Standard @@ -156,15 +145,8 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard //associate external account with registered user await AssociateCustomer(customer, parameters); - //authenticate - if (!approved) - return _customerSettings.UserRegistrationType == UserRegistrationType.AdminApproval - ? new RedirectToRouteResult("RegisterResult", - new { resultId = (int)UserRegistrationType.AdminApproval }) - : Error(["Error on registration"]); - await _authenticationService.SignIn(customer, false); - - return new RedirectToRouteResult("RegisterResult", new { resultId = (int)UserRegistrationType.Standard }); + //Disabled never gets here, so the only unapproved registration type is AdminApproval + return approved ? ExternalAuthResult.Registered(customer) : ExternalAuthResult.AwaitingApproval(customer); } /// @@ -174,31 +156,14 @@ _customerSettings.UserRegistrationType is UserRegistrationType.Standard /// Customer of the request before the login (the guest whose cart is merged) /// Store identifier /// Working language of the request - /// URL to which the user will return after authentication /// Result of an authentication - protected virtual async Task LoginUser(Customer user, Customer previousCustomer, string storeId, - Language language, string returnUrl) + protected virtual async Task LoginUser(Customer user, Customer previousCustomer, + string storeId, Language language) { - //raise event + //raise event; the caller signs the user in afterwards await _mediator.Publish(new CustomerLoggedInEvent(user, previousCustomer, storeId, language)); - //authenticate - await _authenticationService.SignIn(user, false); - - if (string.IsNullOrEmpty(returnUrl)) - return new RedirectToRouteResult("HomePage", new { area = "" }); - - return new RedirectResult(returnUrl); - } - - /// - /// Add errors that occurred during authentication - /// - /// Collection of errors - /// Result of an authentication - protected virtual IActionResult Error(IEnumerable errors) - { - return new RedirectToActionResult("ExternalAuthenticationError", "Common", new { Errors = errors }); + return ExternalAuthResult.LoggedIn(user); } #endregion @@ -276,10 +241,9 @@ public virtual bool AuthenticationProviderIsAvailable(string systemName, Custome /// Customer of the request (a guest when nobody is logged in) /// Store of the request /// Working language of the request - /// URL to which the user will return after authentication /// Result of an authentication - public virtual async Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, - Language language, string returnUrl = null) + public virtual async Task Authenticate(ExternalAuthParam parameters, Customer customer, + Store store, Language language) { ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(customer); @@ -287,7 +251,7 @@ public virtual async Task Authenticate(ExternalAuthParam paramete ArgumentNullException.ThrowIfNull(language); if (!AuthenticationProviderIsAvailable(parameters.ProviderSystemName, customer, store)) - return Error(["External authentication method cannot be loaded"]); + return ExternalAuthResult.Error("External authentication method cannot be loaded"); //get current logged-in user var currentLoggedInUser = await _groupService.IsRegistered(customer) ? customer : null; @@ -295,11 +259,10 @@ public virtual async Task Authenticate(ExternalAuthParam paramete //authenticate associated user if already exists var associatedUser = await GetCustomer(parameters); if (associatedUser != null) - return await AuthenticateExistingUser(associatedUser, currentLoggedInUser, customer, store.Id, language, - returnUrl); + return await AuthenticateExistingUser(associatedUser, currentLoggedInUser, customer, store.Id, language); //or associate and authenticate new user - return await AuthenticateNewUser(currentLoggedInUser, parameters, customer, store.Id, language, returnUrl); + return await AuthenticateNewUser(currentLoggedInUser, parameters, customer, store.Id, language); } #endregion diff --git a/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs b/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs index 4d76f2e63..e012ec8fb 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Authentication/IExternalAuthenticationService.cs @@ -2,7 +2,6 @@ using Grand.Domain.Customers; using Grand.Domain.Localization; using Grand.Domain.Stores; -using Microsoft.AspNetCore.Mvc; namespace Grand.Business.Core.Interfaces.Authentication; @@ -14,16 +13,18 @@ public interface IExternalAuthenticationService #region Authentication /// - /// Use passed parameters to authenticate customer + /// Use passed parameters to find, associate or register the customer. Nothing is signed in here: the caller + /// must sign in the customer of a LoggedIn or Registered result (IExternalSignInService in Grand.Web.Common + /// does it). A LoggedIn result has already published CustomerLoggedInEvent, which merges the guest's cart into + /// the customer, so a caller that skips the sign-in leaves the request as a guest with an emptied cart. /// /// External authentication parameters /// Customer of the request (a guest when nobody is logged in) /// Store of the request /// Working language of the request - /// Defines the URL of user return, when successfully authenticated /// Result of an authentication - Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, Language language, - string returnUrl = null); + Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, + Language language); #endregion diff --git a/src/Business/Grand.Business.Core/Utilities/Authentication/ExternalAuthResult.cs b/src/Business/Grand.Business.Core/Utilities/Authentication/ExternalAuthResult.cs new file mode 100644 index 000000000..17547ec3e --- /dev/null +++ b/src/Business/Grand.Business.Core/Utilities/Authentication/ExternalAuthResult.cs @@ -0,0 +1,64 @@ +using Grand.Domain.Customers; + +namespace Grand.Business.Core.Utilities.Authentication; + +public enum ExternalAuthStatus +{ + /// An existing customer associated with the external account is to be signed in + LoggedIn, + + /// A new customer was registered, approved and is to be signed in + Registered, + + /// The external account is associated with the customer already signed in; nothing to sign in + Associated, + + /// A new customer was registered and waits for admin approval + AwaitingApproval, + + /// The external account belongs to another customer than the one signed in + AlreadyAssigned, + + /// The external account is unknown and registration of new customers is disabled + RegistrationDisabled, + + /// The authentication failed; says why + Error +} + +/// +/// Outcome of an external authentication; signing in, redirecting and the texts shown to the customer are left +/// to the caller +/// +public class ExternalAuthResult +{ + private ExternalAuthResult(ExternalAuthStatus status, Customer customer, IReadOnlyList errors) + { + Status = status; + Customer = customer; + Errors = errors; + } + + public ExternalAuthStatus Status { get; } + + /// The customer to sign in (LoggedIn, Registered) or the one the account was associated with + public Customer Customer { get; } + + /// Technical reasons of an Error result; empty for every other status + public IReadOnlyList Errors { get; } + + public static ExternalAuthResult LoggedIn(Customer customer) => new(ExternalAuthStatus.LoggedIn, customer, []); + public static ExternalAuthResult Registered(Customer customer) => new(ExternalAuthStatus.Registered, customer, []); + public static ExternalAuthResult Associated(Customer customer) => new(ExternalAuthStatus.Associated, customer, []); + + public static ExternalAuthResult AwaitingApproval(Customer customer) => + new(ExternalAuthStatus.AwaitingApproval, customer, []); + + public static ExternalAuthResult AlreadyAssigned() => + new(ExternalAuthStatus.AlreadyAssigned, null, []); + + public static ExternalAuthResult RegistrationDisabled() => + new(ExternalAuthStatus.RegistrationDisabled, null, []); + + public static ExternalAuthResult Error(string error) => new(ExternalAuthStatus.Error, null, [error]); +} diff --git a/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs b/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs index 5dec5505b..1d7dbab80 100644 --- a/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs +++ b/src/Plugins/Authentication.Facebook/Controllers/FacebookAuthenticationController.cs @@ -4,6 +4,7 @@ using Grand.Infrastructure; using Grand.SharedKernel; using Grand.Web.Common.Controllers; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Facebook; using Microsoft.AspNetCore.Mvc; @@ -18,10 +19,12 @@ public class FacebookAuthenticationController : BasePluginController public FacebookAuthenticationController( IExternalAuthenticationService externalAuthenticationService, + IExternalSignInService externalSignInService, IConfiguration configuration, IContextAccessor contextAccessor) { _externalAuthenticationService = externalAuthenticationService; + _externalSignInService = externalSignInService; _configuration = configuration; _contextAccessor = contextAccessor; } @@ -31,6 +34,7 @@ public FacebookAuthenticationController( #region Fields private readonly IExternalAuthenticationService _externalAuthenticationService; + private readonly IExternalSignInService _externalSignInService; private readonly IConfiguration _configuration; private readonly IContextAccessor _contextAccessor; @@ -76,7 +80,7 @@ public async Task FacebookLoginCallback(string returnUrl) }; //authenticate Grand user - return await _externalAuthenticationService.Authenticate(authenticationParameters, + return await _externalSignInService.Authenticate(authenticationParameters, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage, returnUrl); } diff --git a/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs b/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs index 686bec888..82b84725e 100644 --- a/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs +++ b/src/Plugins/Authentication.Google/Controllers/GoogleAuthenticationController.cs @@ -4,6 +4,7 @@ using Grand.Infrastructure; using Grand.SharedKernel; using Grand.Web.Common.Controllers; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Mvc; @@ -17,10 +18,12 @@ public class GoogleAuthenticationController : BasePluginController #region Ctor public GoogleAuthenticationController(IExternalAuthenticationService externalAuthenticationService, + IExternalSignInService externalSignInService, IConfiguration configuration, IContextAccessor contextAccessor) { _externalAuthenticationService = externalAuthenticationService; + _externalSignInService = externalSignInService; _configuration = configuration; _contextAccessor = contextAccessor; } @@ -30,6 +33,7 @@ public GoogleAuthenticationController(IExternalAuthenticationService externalAut #region Fields private readonly IExternalAuthenticationService _externalAuthenticationService; + private readonly IExternalSignInService _externalSignInService; private readonly IConfiguration _configuration; private readonly IContextAccessor _contextAccessor; @@ -75,7 +79,7 @@ public async Task GoogleLoginCallback(string returnUrl) }; //authenticate grand user - return await _externalAuthenticationService.Authenticate(authenticationParameters, + return await _externalSignInService.Authenticate(authenticationParameters, _contextAccessor.WorkContext.CurrentCustomer, _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage, returnUrl); } diff --git a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs index d37e88bc3..f538c9610 100644 --- a/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs +++ b/src/Plugins/Authentication.OpenIdConnect/Controllers/OidcLoginController.cs @@ -8,6 +8,7 @@ using Grand.Infrastructure.Configuration; using Grand.Mediator; using Grand.Web.Common.Controllers; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Common.Security.ExternalRegistration; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; diff --git a/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs b/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs index 3a9e78621..e6eb88022 100644 --- a/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs +++ b/src/Tests/Grand.Business.Authentication.Tests/Services/ExternalAuthenticationServiceTests.cs @@ -19,7 +19,6 @@ namespace Grand.Business.Authentication.Tests.Services; [TestClass] public class ExternalAuthenticationServiceTests { - private Mock _authenticationServiceMock; private Mock _customerManagerServiceMock; private Mock _customerServiceMock; private IEnumerable _externalAuthenticationProviders; @@ -34,24 +33,27 @@ public void Init() { _customerServiceMock = new Mock(); _groupServiceMock = new Mock(); - _authenticationServiceMock = new Mock(); _customerManagerServiceMock = new Mock(); _mediatorMock = new Mock(); _externalAuthenticationRecordRepository = new MongoDBRepositoryTest(); _externalAuthenticationProviders = new List { new ExternalAuthenticationProviderTest() }; + _externalAuthenticationService = CreateService(UserRegistrationType.Standard); + } + + private ExternalAuthenticationService CreateService(UserRegistrationType userRegistrationType) + { var externalAuthenticationSetting = new ExternalAuthenticationSettings { ActiveAuthenticationMethodSystemNames = [ "ExternalAuthenticationProviderTest" ] }; - _externalAuthenticationService = new ExternalAuthenticationService(_authenticationServiceMock.Object, - _customerManagerServiceMock.Object, _customerServiceMock.Object, + return new ExternalAuthenticationService(_customerManagerServiceMock.Object, _customerServiceMock.Object, _groupServiceMock.Object, _mediatorMock.Object, _externalAuthenticationRecordRepository, _externalAuthenticationProviders, - new CustomerSettings { UserRegistrationType = UserRegistrationType.Standard }, externalAuthenticationSetting); + new CustomerSettings { UserRegistrationType = userRegistrationType }, externalAuthenticationSetting); } @@ -133,12 +135,14 @@ public async Task Authenticate_AssociatedUserFromGuest_PublishesLoggedInEventWit var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "1" }; await _externalAuthenticationService.AssociateCustomer(user, parameters); - await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }, language); + var result = + await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }, language); _mediatorMock.Verify(m => m.Publish(It.Is(e => e.Customer == user && e.PreviousCustomer == guest && e.StoreId == "store1" && e.Language == language), default), Times.Once); - _authenticationServiceMock.Verify(a => a.SignIn(user, false), Times.Once); + Assert.AreEqual(ExternalAuthStatus.LoggedIn, result.Status); + Assert.AreSame(user, result.Customer); } [TestMethod] @@ -150,15 +154,87 @@ public async Task Authenticate_UnknownIdentity_RegistersPassedCustomerInPassedSt ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "new", Email = "new@test.com" }; - await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }, language); + var result = + await _externalAuthenticationService.Authenticate(parameters, guest, new Store { Id = "store1" }, language); _customerManagerServiceMock.Verify(m => m.RegisterCustomer(It.Is(r => r.Customer == guest && r.StoreId == "store1" && r.Email == "new@test.com")), Times.Once); _mediatorMock.Verify(m => m.Publish(It.Is(e => e.Customer == guest && e.Language == language), default), Times.Once); - _authenticationServiceMock.Verify(a => a.SignIn(guest, false), Times.Once); Assert.AreEqual("guest", _externalAuthenticationRecordRepository.Table.Single().CustomerId); + Assert.AreEqual(ExternalAuthStatus.Registered, result.Status); + Assert.AreSame(guest, result.Customer); + } + + [TestMethod] + public async Task Authenticate_UnknownIdentityWithAdminApproval_IsAwaitingApproval() + { + var guest = new Customer { Id = "guest" }; + var parameters = new ExternalAuthParam { + ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "new", Email = "new@test.com" + }; + + var result = await CreateService(UserRegistrationType.AdminApproval) + .Authenticate(parameters, guest, new Store { Id = "store1" }, new Language()); + + _customerManagerServiceMock.Verify(m => m.RegisterCustomer(It.Is(r => + r.Customer == guest && !r.IsApproved)), Times.Once); + Assert.AreEqual(ExternalAuthStatus.AwaitingApproval, result.Status); + } + + [TestMethod] + public async Task Authenticate_UnknownIdentityWithRegistrationDisabled_RegistersNobody() + { + var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "new" }; + + var result = await CreateService(UserRegistrationType.Disabled) + .Authenticate(parameters, new Customer { Id = "guest" }, new Store(), new Language()); + + _customerManagerServiceMock.Verify(m => m.RegisterCustomer(It.IsAny()), Times.Never); + Assert.AreEqual(ExternalAuthStatus.RegistrationDisabled, result.Status); + Assert.IsFalse(_externalAuthenticationRecordRepository.Table.Any()); + } + + [TestMethod] + public async Task Authenticate_UnknownIdentityWhileLoggedIn_AssociatesItWithLoggedInCustomer() + { + var customer = new Customer { Id = "registered" }; + _groupServiceMock.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true); + var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "new" }; + + var result = await _externalAuthenticationService.Authenticate(parameters, customer, new Store(), new Language()); + + Assert.AreEqual(ExternalAuthStatus.Associated, result.Status); + Assert.AreEqual("registered", _externalAuthenticationRecordRepository.Table.Single().CustomerId); + _mediatorMock.Verify(m => m.Publish(It.IsAny(), default), Times.Never); + } + + [TestMethod] + public async Task Authenticate_IdentityOfAnotherCustomerWhileLoggedIn_IsAlreadyAssigned() + { + var owner = new Customer { Id = "owner" }; + var customer = new Customer { Id = "registered" }; + _groupServiceMock.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true); + _customerServiceMock.Setup(c => c.GetCustomerById("owner")).ReturnsAsync(owner); + var parameters = new ExternalAuthParam { ProviderSystemName = "ExternalAuthenticationProviderTest", Identifier = "1" }; + await _externalAuthenticationService.AssociateCustomer(owner, parameters); + + var result = await _externalAuthenticationService.Authenticate(parameters, customer, new Store(), new Language()); + + Assert.AreEqual(ExternalAuthStatus.AlreadyAssigned, result.Status); + Assert.IsEmpty(result.Errors); + } + + [TestMethod] + public async Task Authenticate_UnavailableProvider_IsError() + { + var parameters = new ExternalAuthParam { ProviderSystemName = "Unknown", Identifier = "1" }; + + var result = await _externalAuthenticationService.Authenticate(parameters, new Customer(), new Store(), new Language()); + + Assert.AreEqual(ExternalAuthStatus.Error, result.Status); + _customerManagerServiceMock.Verify(m => m.RegisterCustomer(It.IsAny()), Times.Never); } [TestMethod] diff --git a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs index 214a2b85f..a5964643f 100644 --- a/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs +++ b/src/Tests/Grand.Modules.Tests/Plugins/OpenIdConnect/OidcLoginControllerTests.cs @@ -14,6 +14,7 @@ using Grand.Domain.Stores; using Grand.Infrastructure; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Common.Security.ExternalRegistration; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs index b214fbc41..aff51054d 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerExternalSignInTests.cs @@ -9,6 +9,7 @@ using Grand.Mediator; using Grand.Web.Admin.Controllers; using Grand.Web.AdminShared.Models.Common; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs index 103bacd20..f46550f2c 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/LoginControllerProviderOnlyTests.cs @@ -9,6 +9,7 @@ using Grand.Mediator; using Grand.Web.Admin.Controllers; using Grand.Web.AdminShared.Models.Common; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; diff --git a/src/Tests/Grand.Web.Common.Tests/RequestCustomerResolversTests.cs b/src/Tests/Grand.Web.Common.Tests/RequestCustomerResolversTests.cs index dce7834e5..7c86e47a0 100644 --- a/src/Tests/Grand.Web.Common.Tests/RequestCustomerResolversTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/RequestCustomerResolversTests.cs @@ -1,4 +1,3 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Customers; diff --git a/src/Tests/Grand.Web.Common.Tests/Security/Authentication/ExternalSignInServiceTests.cs b/src/Tests/Grand.Web.Common.Tests/Security/Authentication/ExternalSignInServiceTests.cs new file mode 100644 index 000000000..5cfa4d456 --- /dev/null +++ b/src/Tests/Grand.Web.Common.Tests/Security/Authentication/ExternalSignInServiceTests.cs @@ -0,0 +1,148 @@ +using Grand.Business.Core.Interfaces.Authentication; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Authentication; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; +using Grand.Web.Common.Security.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Common.Tests.Security.Authentication; + +[TestClass] +public class ExternalSignInServiceTests +{ + private readonly Customer _customer = new() { Id = "customer" }; + private Mock _authenticationServiceMock; + private Mock _externalAuthenticationServiceMock; + private Mock _translationServiceMock; + private ExternalSignInService _service; + + [TestInitialize] + public void Init() + { + _externalAuthenticationServiceMock = new Mock(); + _authenticationServiceMock = new Mock(); + _translationServiceMock = new Mock(); + _translationServiceMock.Setup(t => t.GetResource(It.IsAny(), "lang1", It.IsAny(), + It.IsAny())).Returns((key, _, _, _) => $"{key}@lang1"); + _service = new ExternalSignInService(_externalAuthenticationServiceMock.Object, + _authenticationServiceMock.Object, _translationServiceMock.Object); + } + + private Task Authenticate(ExternalAuthResult result, string returnUrl = null) + { + _externalAuthenticationServiceMock + .Setup(s => s.Authenticate(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny())) + .ReturnsAsync(result); + return _service.Authenticate(new ExternalAuthParam(), new Customer(), new Store(), new Language { Id = "lang1" }, + returnUrl); + } + + [TestMethod] + public async Task LoggedIn_SignsInAndRedirectsToReturnUrl() + { + var result = await Authenticate(ExternalAuthResult.LoggedIn(_customer), "/cart"); + + _authenticationServiceMock.Verify(a => a.SignIn(_customer, false), Times.Once); + Assert.AreEqual("/cart", ((RedirectResult)result).Url); + } + + [TestMethod] + public async Task LoggedIn_WithoutReturnUrl_RedirectsToHomePage() + { + var result = await Authenticate(ExternalAuthResult.LoggedIn(_customer)); + + Assert.AreEqual("HomePage", ((RedirectToRouteResult)result).RouteName); + } + + [TestMethod] + public async Task Registered_SignsInAndShowsStandardRegisterResult() + { + var result = await Authenticate(ExternalAuthResult.Registered(_customer), "/cart"); + + _authenticationServiceMock.Verify(a => a.SignIn(_customer, false), Times.Once); + var redirect = (RedirectToRouteResult)result; + Assert.AreEqual("RegisterResult", redirect.RouteName); + Assert.AreEqual((int)UserRegistrationType.Standard, redirect.RouteValues!["resultId"]); + } + + [TestMethod] + public async Task Associated_RedirectsToReturnUrlWithoutSignIn() + { + var result = await Authenticate(ExternalAuthResult.Associated(_customer), "/account"); + + _authenticationServiceMock.Verify(a => a.SignIn(It.IsAny(), It.IsAny()), Times.Never); + Assert.AreEqual("/account", ((RedirectResult)result).Url); + } + + [TestMethod] + public async Task AwaitingApproval_ShowsAdminApprovalRegisterResultWithoutSignIn() + { + var result = await Authenticate(ExternalAuthResult.AwaitingApproval(_customer)); + + _authenticationServiceMock.Verify(a => a.SignIn(It.IsAny(), It.IsAny()), Times.Never); + var redirect = (RedirectToRouteResult)result; + Assert.AreEqual("RegisterResult", redirect.RouteName); + Assert.AreEqual((int)UserRegistrationType.AdminApproval, redirect.RouteValues!["resultId"]); + } + + [TestMethod] + [DataRow("https://evil.example")] + [DataRow("//evil.example")] + [DataRow("/\evil.example")] + [DataRow("javascript:alert(1)")] + public async Task LoggedIn_WithExternalReturnUrl_RedirectsToHomePage(string returnUrl) + { + var result = await Authenticate(ExternalAuthResult.LoggedIn(_customer), returnUrl); + + _authenticationServiceMock.Verify(a => a.SignIn(_customer, false), Times.Once); + Assert.AreEqual("HomePage", ((RedirectToRouteResult)result).RouteName); + } + + [TestMethod] + public async Task Associated_WithExternalReturnUrl_RedirectsToHomePage() + { + var result = await Authenticate(ExternalAuthResult.Associated(_customer), "https://evil.example"); + + Assert.AreEqual("HomePage", ((RedirectToRouteResult)result).RouteName); + } + + [TestMethod] + public async Task AlreadyAssigned_ShowsTranslatedErrorWithoutSignIn() + { + var result = (RedirectToActionResult)await Authenticate(ExternalAuthResult.AlreadyAssigned()); + + Assert.AreEqual("ExternalAuthenticationError", result.ActionName); + Assert.AreEqual("Common", result.ControllerName); + CollectionAssert.AreEqual(new[] { "Account.AssociatedExternalAuth.AlreadyAssigned@lang1" }, + ((IEnumerable)result.RouteValues!["Errors"]).ToArray()); + _authenticationServiceMock.Verify(a => a.SignIn(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task RegistrationDisabled_ShowsTranslatedErrorWithoutSignIn() + { + var result = (RedirectToActionResult)await Authenticate(ExternalAuthResult.RegistrationDisabled()); + + Assert.AreEqual("ExternalAuthenticationError", result.ActionName); + CollectionAssert.AreEqual(new[] { "Account.AssociatedExternalAuth.RegistrationDisabled@lang1" }, + ((IEnumerable)result.RouteValues!["Errors"]).ToArray()); + _authenticationServiceMock.Verify(a => a.SignIn(It.IsAny(), It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task Error_ShowsErrorsOfTheResultWithoutSignIn() + { + var error = ExternalAuthResult.Error("failed"); + + var result = (RedirectToActionResult)await Authenticate(error); + + Assert.AreEqual("ExternalAuthenticationError", result.ActionName); + Assert.AreSame(error.Errors, result.RouteValues!["Errors"]); + _authenticationServiceMock.Verify(a => a.SignIn(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Tests/Grand.Business.Authentication.Tests/Services/GrandAuthenticationServiceDefaultsTests.cs b/src/Tests/Grand.Web.Common.Tests/Security/Authentication/GrandAuthenticationServiceDefaultsTests.cs similarity index 90% rename from src/Tests/Grand.Business.Authentication.Tests/Services/GrandAuthenticationServiceDefaultsTests.cs rename to src/Tests/Grand.Web.Common.Tests/Security/Authentication/GrandAuthenticationServiceDefaultsTests.cs index 119b1616a..315af4d34 100644 --- a/src/Tests/Grand.Business.Authentication.Tests/Services/GrandAuthenticationServiceDefaultsTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/Security/Authentication/GrandAuthenticationServiceDefaultsTests.cs @@ -1,8 +1,8 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Domain.Customers; +using Grand.Web.Common.Security.Authentication; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Grand.Business.Authentication.Tests.Services; +namespace Grand.Web.Common.Tests.Security.Authentication; [TestClass] public class GrandAuthenticationServiceDefaultsTests diff --git a/src/Tests/Grand.Web.Common.Tests/Security/ExternalRegistration/ProviderSignOutTests.cs b/src/Tests/Grand.Web.Common.Tests/Security/ExternalRegistration/ProviderSignOutTests.cs index 79a1081c2..fa897659e 100644 --- a/src/Tests/Grand.Web.Common.Tests/Security/ExternalRegistration/ProviderSignOutTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/Security/ExternalRegistration/ProviderSignOutTests.cs @@ -2,6 +2,7 @@ using Grand.Domain.Stores; using Grand.Infrastructure; using Grand.Infrastructure.Configuration; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Common.Security.ExternalRegistration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; diff --git a/src/Tests/Grand.Web.Common.Tests/Security/Oidc/OidcWorkContextTests.cs b/src/Tests/Grand.Web.Common.Tests/Security/Oidc/OidcWorkContextTests.cs index 7d3bc8fe5..1afc271b9 100644 --- a/src/Tests/Grand.Web.Common.Tests/Security/Oidc/OidcWorkContextTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/Security/Oidc/OidcWorkContextTests.cs @@ -1,4 +1,3 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Security; diff --git a/src/Tests/Grand.Web.Common.Tests/WorkContextSetterRequestResolverTests.cs b/src/Tests/Grand.Web.Common.Tests/WorkContextSetterRequestResolverTests.cs index ca9548022..c3842ac01 100644 --- a/src/Tests/Grand.Web.Common.Tests/WorkContextSetterRequestResolverTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/WorkContextSetterRequestResolverTests.cs @@ -1,5 +1,4 @@ using System.Reflection; -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Security; diff --git a/src/Tests/Grand.Web.Common.Tests/WorkContextSetterTests.cs b/src/Tests/Grand.Web.Common.Tests/WorkContextSetterTests.cs index dcc8ce78e..74f4f13e1 100644 --- a/src/Tests/Grand.Web.Common.Tests/WorkContextSetterTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/WorkContextSetterTests.cs @@ -1,4 +1,3 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Security; @@ -8,6 +7,7 @@ using Grand.Domain.Stores; using Grand.Domain.Tax; using Grand.Infrastructure.Configuration; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/Tests/Grand.Web.Tests/Commands/Customers/UpdateCustomerInfoCommandHandlerTests.cs b/src/Tests/Grand.Web.Tests/Commands/Customers/UpdateCustomerInfoCommandHandlerTests.cs index 3fa9f7e34..b5b033ed8 100644 --- a/src/Tests/Grand.Web.Tests/Commands/Customers/UpdateCustomerInfoCommandHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Commands/Customers/UpdateCustomerInfoCommandHandlerTests.cs @@ -1,4 +1,3 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Catalog.Tax; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Marketing.Newsletters; @@ -7,6 +6,7 @@ using Grand.Infrastructure.Configuration; using Grand.Web.Commands.Handler.Customers; using Grand.Web.Commands.Models.Customers; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Models.Customer; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs index 74f3b7679..7f9d5d0f0 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerExternalRegisterTests.cs @@ -10,6 +10,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Common.Security.ExternalRegistration; using Grand.Web.Controllers; using Grand.Web.Features.Models.Customers; diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs index 2e5b9f4de..45fe8eff2 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerProviderOnlyTests.cs @@ -9,6 +9,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Common.Security.ExternalRegistration; using Grand.Web.Controllers; using Grand.Web.Features.Models.Customers; diff --git a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs index f9ebadfae..433002287 100644 --- a/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs +++ b/src/Tests/Grand.Web.Tests/Controllers/AccountControllerTwoFactorReturnUrlTests.cs @@ -9,6 +9,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Controllers; using Grand.Web.Models.Customer; using Microsoft.AspNetCore.Http; diff --git a/src/Web/Grand.Web.Admin/Controllers/LoginController.cs b/src/Web/Grand.Web.Admin/Controllers/LoginController.cs index e5844fb6a..ed8a66edd 100644 --- a/src/Web/Grand.Web.Admin/Controllers/LoginController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/LoginController.cs @@ -1,5 +1,4 @@ -using Grand.Business.Core.Interfaces.Authentication; -using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Domain.Common; using Grand.Domain.Customers; @@ -7,6 +6,7 @@ using Grand.Web.Admin.Extensions; using Grand.Web.AdminShared.Controllers; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Mvc; using Grand.Web.Common.Localization; diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index c8a990e4c..21333964d 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -12,6 +12,7 @@ using Grand.Web.AdminShared.Models.Common; using Grand.Web.Common.Controllers; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Grand.Web.Common.Localization; diff --git a/src/Web/Grand.Web.Common/RequestCustomerResolvers/CookieCustomerResolver.cs b/src/Web/Grand.Web.Common/RequestCustomerResolvers/CookieCustomerResolver.cs index 5416d4588..c5d11782a 100644 --- a/src/Web/Grand.Web.Common/RequestCustomerResolvers/CookieCustomerResolver.cs +++ b/src/Web/Grand.Web.Common/RequestCustomerResolvers/CookieCustomerResolver.cs @@ -1,5 +1,5 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Domain.Customers; +using Grand.Web.Common.Security.Authentication; namespace Grand.Web.Common.RequestCustomerResolvers; diff --git a/src/Web/Grand.Web.Common/RequestCustomerResolvers/GuestCookieCustomerResolver.cs b/src/Web/Grand.Web.Common/RequestCustomerResolvers/GuestCookieCustomerResolver.cs index 66ea5f1c6..d0c02bfeb 100644 --- a/src/Web/Grand.Web.Common/RequestCustomerResolvers/GuestCookieCustomerResolver.cs +++ b/src/Web/Grand.Web.Common/RequestCustomerResolvers/GuestCookieCustomerResolver.cs @@ -1,7 +1,7 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Customers; using Grand.Domain.Customers; +using Grand.Web.Common.Security.Authentication; namespace Grand.Web.Common.RequestCustomerResolvers; diff --git a/src/Web/Grand.Web.Common/Security/Authentication/ExternalSignInService.cs b/src/Web/Grand.Web.Common/Security/Authentication/ExternalSignInService.cs new file mode 100644 index 000000000..88e8f7061 --- /dev/null +++ b/src/Web/Grand.Web.Common/Security/Authentication/ExternalSignInService.cs @@ -0,0 +1,68 @@ +using Grand.Business.Core.Interfaces.Authentication; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Authentication; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Common.Security.Authentication; + +public class ExternalSignInService( + IExternalAuthenticationService externalAuthenticationService, + IGrandAuthenticationService authenticationService, + ITranslationService translationService) : IExternalSignInService +{ + public virtual async Task Authenticate(ExternalAuthParam parameters, Customer customer, + Store store, Language language, string returnUrl = null) + { + //the logged-in or registered event is already published; sign in after it, as before + var result = await externalAuthenticationService.Authenticate(parameters, customer, store, language); + switch (result.Status) + { + case ExternalAuthStatus.LoggedIn: + await authenticationService.SignIn(result.Customer, false); + return ReturnUrl(returnUrl); + case ExternalAuthStatus.Registered: + await authenticationService.SignIn(result.Customer, false); + return RegisterResult(UserRegistrationType.Standard); + case ExternalAuthStatus.Associated: + return ReturnUrl(returnUrl); + case ExternalAuthStatus.AwaitingApproval: + return RegisterResult(UserRegistrationType.AdminApproval); + case ExternalAuthStatus.AlreadyAssigned: + return Error([ + translationService.GetResource("Account.AssociatedExternalAuth.AlreadyAssigned", language.Id) + ]); + case ExternalAuthStatus.RegistrationDisabled: + return Error([ + translationService.GetResource("Account.AssociatedExternalAuth.RegistrationDisabled", language.Id) + ]); + case ExternalAuthStatus.Error: + return Error(result.Errors); + default: + throw new ArgumentOutOfRangeException(nameof(result.Status), result.Status, + "Unhandled external authentication status"); + } + } + + protected virtual IActionResult ReturnUrl(string returnUrl) + { + //the return URL comes from the query string of the login request; never leave the store with it + if (string.IsNullOrEmpty(returnUrl) || !RedirectHttpResult.IsLocalUrl(returnUrl)) + return new RedirectToRouteResult("HomePage", new { area = "" }); + + return new RedirectResult(returnUrl); + } + + protected virtual IActionResult RegisterResult(UserRegistrationType registrationType) + { + return new RedirectToRouteResult("RegisterResult", new { resultId = (int)registrationType }); + } + + protected virtual IActionResult Error(IEnumerable errors) + { + return new RedirectToActionResult("ExternalAuthenticationError", "Common", new { Errors = errors }); + } +} diff --git a/src/Web/Grand.Web.Common/Security/Authentication/IExternalSignInService.cs b/src/Web/Grand.Web.Common/Security/Authentication/IExternalSignInService.cs new file mode 100644 index 000000000..3e564b3a1 --- /dev/null +++ b/src/Web/Grand.Web.Common/Security/Authentication/IExternalSignInService.cs @@ -0,0 +1,25 @@ +using Grand.Business.Core.Utilities.Authentication; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Stores; +using Microsoft.AspNetCore.Mvc; + +namespace Grand.Web.Common.Security.Authentication; + +/// +/// Completes a sign-in by an external authentication method: signs the customer in and picks where to go next +/// +public interface IExternalSignInService +{ + /// + /// Use passed parameters to authenticate customer + /// + /// External authentication parameters + /// Customer of the request (a guest when nobody is logged in) + /// Store of the request + /// Working language of the request + /// Defines the URL of user return, when successfully authenticated + /// Result of an authentication + Task Authenticate(ExternalAuthParam parameters, Customer customer, Store store, Language language, + string returnUrl = null); +} diff --git a/src/Business/Grand.Business.Core/Interfaces/Authentication/IGrandAuthenticationService.cs b/src/Web/Grand.Web.Common/Security/Authentication/IGrandAuthenticationService.cs similarity index 96% rename from src/Business/Grand.Business.Core/Interfaces/Authentication/IGrandAuthenticationService.cs rename to src/Web/Grand.Web.Common/Security/Authentication/IGrandAuthenticationService.cs index 01cd7254e..ac038041c 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Authentication/IGrandAuthenticationService.cs +++ b/src/Web/Grand.Web.Common/Security/Authentication/IGrandAuthenticationService.cs @@ -1,6 +1,6 @@ using Grand.Domain.Customers; -namespace Grand.Business.Core.Interfaces.Authentication; +namespace Grand.Web.Common.Security.Authentication; /// /// Authentication service interface diff --git a/src/Web/Grand.Web.Common/Security/ExternalRegistration/ProviderSignOut.cs b/src/Web/Grand.Web.Common/Security/ExternalRegistration/ProviderSignOut.cs index 609da5cc8..a854309ae 100644 --- a/src/Web/Grand.Web.Common/Security/ExternalRegistration/ProviderSignOut.cs +++ b/src/Web/Grand.Web.Common/Security/ExternalRegistration/ProviderSignOut.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Authentication; using Grand.Infrastructure; using Grand.Infrastructure.Configuration; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; diff --git a/src/Web/Grand.Web.Common/Startup/StartupApplication.cs b/src/Web/Grand.Web.Common/Startup/StartupApplication.cs index 32341410d..1366816e1 100644 --- a/src/Web/Grand.Web.Common/Startup/StartupApplication.cs +++ b/src/Web/Grand.Web.Common/Startup/StartupApplication.cs @@ -1,4 +1,3 @@ -using Grand.Business.Core.Interfaces.Authentication; using Grand.Business.Core.Interfaces.Common.Pdf; using Grand.Business.Core.Interfaces.System.Admin; using Grand.Data; @@ -125,6 +124,7 @@ private static void RegisterFramework(IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); diff --git a/src/Web/Grand.Web.Common/WorkContextSetter.cs b/src/Web/Grand.Web.Common/WorkContextSetter.cs index aeba6b8a8..5aafafffb 100644 --- a/src/Web/Grand.Web.Common/WorkContextSetter.cs +++ b/src/Web/Grand.Web.Common/WorkContextSetter.cs @@ -1,5 +1,4 @@ -using Grand.Business.Core.Interfaces.Authentication; -using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Security; using Grand.Business.Core.Interfaces.Common.Stores; @@ -14,6 +13,7 @@ using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Web.Common.RequestCustomerResolvers; +using Grand.Web.Common.Security.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Web/Grand.Web.Store/Controllers/LoginController.cs b/src/Web/Grand.Web.Store/Controllers/LoginController.cs index 72650e744..667be5ad1 100644 --- a/src/Web/Grand.Web.Store/Controllers/LoginController.cs +++ b/src/Web/Grand.Web.Store/Controllers/LoginController.cs @@ -1,10 +1,10 @@ -using Grand.Business.Core.Interfaces.Authentication; -using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Infrastructure; using Grand.Web.AdminShared.Controllers; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Store.Extensions; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; diff --git a/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs b/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs index d963d6493..4aef6cbde 100644 --- a/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs +++ b/src/Web/Grand.Web.Vendor/Controllers/LoginController.cs @@ -1,10 +1,10 @@ -using Grand.Business.Core.Interfaces.Authentication; -using Grand.Business.Core.Interfaces.Customers; +using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Messages; using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Infrastructure; using Grand.Web.AdminShared.Controllers; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Vendor.Extensions; using Grand.Mediator; using Microsoft.AspNetCore.Mvc; diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index f3f1813fd..82316f2f3 100644 Binary files a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml and b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml differ diff --git a/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml b/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml index 0d79b1a7d..503982d63 100644 --- a/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml +++ b/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml @@ -60,4 +60,10 @@ The account links now belong to the configured identity provider. + + This external account is already connected to another customer account. + + + Registration of new customers is disabled, so this external account cannot be used to create one. + diff --git a/src/Web/Grand.Web/Commands/Handler/Customers/UpdateCustomerInfoCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Customers/UpdateCustomerInfoCommandHandler.cs index 353491b7c..892b9acd5 100644 --- a/src/Web/Grand.Web/Commands/Handler/Customers/UpdateCustomerInfoCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Customers/UpdateCustomerInfoCommandHandler.cs @@ -1,5 +1,4 @@ -using Grand.Business.Core.Interfaces.Authentication; -using Grand.Business.Core.Interfaces.Catalog.Tax; +using Grand.Business.Core.Interfaces.Catalog.Tax; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Marketing.Newsletters; using Grand.Domain.Common; @@ -9,6 +8,7 @@ using Grand.Infrastructure.Configuration; using Grand.Web.Commands.Models.Customers; using Grand.Mediator; +using Grand.Web.Common.Security.Authentication; namespace Grand.Web.Commands.Handler.Customers; diff --git a/src/Web/Grand.Web/Controllers/AccountController.cs b/src/Web/Grand.Web/Controllers/AccountController.cs index 92f98fbc0..cc7321e4e 100644 --- a/src/Web/Grand.Web/Controllers/AccountController.cs +++ b/src/Web/Grand.Web/Controllers/AccountController.cs @@ -16,6 +16,7 @@ using Grand.Web.Commands.Models.Customers; using Grand.Web.Common.Controllers; using Grand.Web.Common.Filters; +using Grand.Web.Common.Security.Authentication; using Grand.Web.Common.Security.Authorization; using Grand.Web.Common.Security.Captcha; using Grand.Web.Common.Security.ExternalRegistration;