Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,19 @@ await _httpContextAccessor.HttpContext.SignOutAsync(GrandCookieAuthenticationDef
}
}

/// <summary>
/// Whether the current sign-in cookie references a session started at the identity provider
/// </summary>
public virtual async Task<bool> HasOidcSession()
{
if (_httpContextAccessor.HttpContext == null)
return false;

var authenticateResult = await _httpContextAccessor.HttpContext.AuthenticateAsync(
GrandCookieAuthenticationDefaults.AuthenticationScheme);
return authenticateResult?.Succeeded == true && !string.IsNullOrEmpty(OidcSessionId(authenticateResult.Principal));
}

/// <summary>
/// Get an authenticated customer
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Grand.Business.Core.Interfaces.Authentication;

/// <summary>An external authentication provider that can end the user's session at the provider (RP-initiated logout)</summary>
public interface IExternalSignOutProvider
{
/// <summary>
/// Absolute URL that ends the session at the provider and returns to <paramref name="postLogoutRedirectUri" />
/// (registered at the provider); null when the provider cannot do it now
/// </summary>
Task<string> GetSignOutUrl(string postLogoutRedirectUri);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public interface IGrandAuthenticationService
/// </summary>
Task SignOut();

/// <summary>
/// Whether the current sign-in cookie references a session started at the identity provider; an implementation
/// without provider sessions keeps the default (no)
/// </summary>
Task<bool> HasOidcSession() => Task.FromResult(false);

/// <summary>
/// Get authenticated customer
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,22 @@ public async Task SignOut_EndsOidcSession()
_oidcSessionService.Verify(x => x.End("session-1", OidcSessionEndReason.SignOut), Times.Once);
}

[TestMethod]
public async Task HasOidcSession_SessionCookie_True()
{
_authServiceMock.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), GrandCookieAuthenticationDefaults.AuthenticationScheme))
.ReturnsAsync(Ticket(new Claim(OidcSessionDefaults.SessionIdClaimType, "session-1", ClaimValueTypes.String, "grandnode")));
Assert.IsTrue(await _cookieAuthService.HasOidcSession());
}

[TestMethod]
public async Task HasOidcSession_PlainCookie_False()
{
_authServiceMock.Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), GrandCookieAuthenticationDefaults.AuthenticationScheme))
.ReturnsAsync(Ticket(new Claim(ClaimTypes.Email, "a@b.c", ClaimValueTypes.Email, "grandnode")));
Assert.IsFalse(await _cookieAuthService.HasOidcSession());
}

private (Customer customer, Func<ClaimsPrincipal> signedIn) PrepareSignIn(string customerId)
{
ClaimsPrincipal signedIn = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Domain.Customers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Business.Authentication.Tests.Services;

[TestClass]
public class GrandAuthenticationServiceDefaultsTests
{
[TestMethod]
public async Task HasOidcSession_ImplementationWithoutIt_ReturnsFalse()
{
IGrandAuthenticationService service = new LegacyAuthenticationService();

Assert.IsFalse(await service.HasOidcSession());
}

//an implementation written before HasOidcSession existed
private sealed class LegacyAuthenticationService : IGrandAuthenticationService
{
public Task SignIn(Customer customer, bool createPersistentCookie) => Task.CompletedTask;

public Task SignIn(Customer customer, bool createPersistentCookie, string oidcSessionId) => Task.CompletedTask;

public Task SignOut() => Task.CompletedTask;

public Task<Customer> GetAuthenticatedCustomer() => Task.FromResult<Customer>(null);

public Task<string> GetCustomerGuid() => Task.FromResult<string>(null);

public Task SetCustomerGuid(Guid customerGuid) => Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -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.Customers;
Expand All @@ -8,6 +7,7 @@
using Grand.Domain.Stores;
using Grand.Infrastructure;
using Grand.Web.AdminShared.Controllers;
using Grand.Web.Common.Security.ExternalRegistration;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
Expand All @@ -24,10 +24,9 @@ public class BaseHomeControllerTests
private class TestHomeController(
ICountryService countryService,
ITranslationService translationService,
IGrandAuthenticationService authenticationService,
string selectStateKey,
string logoutRoute)
: BaseHomeController(countryService, translationService, authenticationService)
: BaseHomeController(countryService, translationService)
{
protected override string SelectStateResourceKey => selectStateKey;
protected override string LogoutRouteName => logoutRoute;
Expand All @@ -36,12 +35,11 @@ private class TestHomeController(
private class TestHomeControllerWithSetLanguage(
ICountryService countryService,
ITranslationService translationService,
IGrandAuthenticationService authenticationService,
IContextAccessor contextAccessor,
string selectStateKey,
string logoutRoute,
string areaName)
: BaseHomeControllerWithSetLanguage(countryService, translationService, authenticationService, contextAccessor)
: BaseHomeControllerWithSetLanguage(countryService, translationService, contextAccessor)
{
protected override string SelectStateResourceKey => selectStateKey;
protected override string LogoutRouteName => logoutRoute;
Expand All @@ -50,15 +48,13 @@ private class TestHomeControllerWithSetLanguage(

private Mock<ICountryService> _countryServiceMock = null!;
private Mock<ITranslationService> _translationServiceMock = null!;
private Mock<IGrandAuthenticationService> _authenticationServiceMock = null!;

[TestInitialize]
public void Setup()
{
_countryServiceMock = new Mock<ICountryService>();
_translationServiceMock = new Mock<ITranslationService>();
_translationServiceMock.Setup(t => t.GetResource(It.IsAny<string>())).Returns((string key) => key);
_authenticationServiceMock = new Mock<IGrandAuthenticationService>();
}

private static void WireControllerInfrastructure(Controller controller)
Expand All @@ -79,7 +75,7 @@ private static void WireControllerInfrastructure(Controller controller)
private TestHomeController CreateController(string selectStateKey, string logoutRoute)
{
var controller = new TestHomeController(_countryServiceMock.Object, _translationServiceMock.Object,
_authenticationServiceMock.Object, selectStateKey, logoutRoute);
selectStateKey, logoutRoute);
WireControllerInfrastructure(controller);
return controller;
}
Expand Down Expand Up @@ -150,12 +146,26 @@ public async Task Logout_SignsOutAndRedirectsToSubclassRoute()
{
var controller = CreateController("Admin.Address.SelectState", "VendorLogin");

var result = (RedirectToRouteResult)await controller.Logout();
var providerSignOut = new Mock<IProviderSignOut>();

_authenticationServiceMock.Verify(a => a.SignOut(), Times.Once);
var result = (RedirectToRouteResult)await controller.Logout(providerSignOut.Object);

providerSignOut.Verify(a => a.SignOut(), Times.Once);
Assert.AreEqual("VendorLogin", result.RouteName);
}

[TestMethod]
public async Task Logout_ProviderUrl_RedirectsToProvider()
{
var controller = CreateController("Admin.Address.SelectState", "VendorLogin");
var providerSignOut = new Mock<IProviderSignOut>();
providerSignOut.Setup(a => a.SignOut()).ReturnsAsync("https://idp.example/logout");

var result = (RedirectResult)await controller.Logout(providerSignOut.Object);

Assert.AreEqual("https://idp.example/logout", result.Url);
}

[TestMethod]
public async Task SetLanguage_EmptyReturnUrl_RedirectsToSubclassArea()
{
Expand All @@ -173,7 +183,7 @@ public async Task SetLanguage_EmptyReturnUrl_RedirectsToSubclassArea()
languageServiceMock.Setup(l => l.GetLanguageById("lang-1")).ReturnsAsync(new Language { Id = "lang-1" });

var controller = new TestHomeControllerWithSetLanguage(_countryServiceMock.Object,
_translationServiceMock.Object, _authenticationServiceMock.Object, contextAccessorMock.Object,
_translationServiceMock.Object, contextAccessorMock.Object,
"Store.Address.SelectState", "StoreLogin", "Store");
WireControllerInfrastructure(controller);
var urlHelperMock = new Mock<IUrlHelper>();
Expand Down Expand Up @@ -207,7 +217,7 @@ public async Task SetLanguage_NonLocalReturnUrl_RedirectsToSubclassAreaIndexInst
languageServiceMock.Setup(l => l.GetLanguageById(It.IsAny<string>())).ReturnsAsync((Language)null!);

var controller = new TestHomeControllerWithSetLanguage(_countryServiceMock.Object,
_translationServiceMock.Object, _authenticationServiceMock.Object, contextAccessorMock.Object,
_translationServiceMock.Object, contextAccessorMock.Object,
"Store.Address.SelectState", "StoreLogin", "Store");
WireControllerInfrastructure(controller);
var urlHelperMock = new Mock<IUrlHelper>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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.Customers;
using Grand.Business.Core.Interfaces.Messages;
using Grand.Domain.Common;
using Grand.Domain.Customers;
using Grand.Infrastructure;
using Grand.Infrastructure.Configuration;
using Grand.Mediator;
using Grand.Web.Admin.Controllers;
using Grand.Web.AdminShared.Models.Common;
Expand All @@ -21,12 +23,14 @@ namespace Grand.Web.Admin.Tests.Controllers;
public class LoginControllerExternalSignInTests
{
private Mock<IExternalAuthenticationService> _externalAuthenticationService = null!;
private Mock<IGroupService> _groupService = null!;
private LoginController _controller = null!;

[TestInitialize]
public void Setup()
{
_externalAuthenticationService = new Mock<IExternalAuthenticationService>();
_groupService = new Mock<IGroupService>();
_controller = new LoginController(new CustomerSettings(), new CaptchaSettings(),
new Mock<ITranslationService>().Object, new Mock<ICustomerManagerService>().Object,
new Mock<ICustomerService>().Object, new Mock<IGrandAuthenticationService>().Object,
Expand All @@ -53,7 +57,7 @@ public async Task Index_ListsPanelProvidersOnly()
_externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(null, null))
.Returns([panel.Object, storefrontOnly.Object]);

var model = (LoginModel)((ViewResult)await _controller.Index(_externalAuthenticationService.Object)).Model!;
var model = (LoginModel)((ViewResult)await _controller.Index(_externalAuthenticationService.Object, new OidcLoginConfig(), _groupService.Object)).Model!;

Assert.AreEqual(1, model.ExternalSignIn.Count);
Assert.AreEqual("OpenID Connect", model.ExternalSignIn[0].Name);
Expand All @@ -66,7 +70,7 @@ public async Task Index_ProviderWithoutPath_Hidden()
var panel = new Mock<IExternalSignInPathProvider>();
panel.Setup(x => x.GetSignInPath(It.IsAny<string>())).ReturnsAsync((string)null!);
_externalAuthenticationService.Setup(x => x.LoadActiveAuthenticationProviders(null, null)).Returns([panel.Object]);
var model = (LoginModel)((ViewResult)await _controller.Index(_externalAuthenticationService.Object)).Model!;
var model = (LoginModel)((ViewResult)await _controller.Index(_externalAuthenticationService.Object, new OidcLoginConfig(), _groupService.Object)).Model!;
Assert.AreEqual(0, model.ExternalSignIn.Count);
}
}
Loading
Loading