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 @@ -17,9 +17,6 @@ public class StartupApplication : IStartupApplication
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IGrandAuthenticationService, CookieAuthenticationService>();
services.AddScoped<ICookieOptionsFactory, CookieOptionsFactory>();
services.AddScoped<IApiAuthenticationService, ApiAuthenticationService>();
services.AddScoped<IJwtBearerAuthenticationService, JwtBearerAuthenticationService>();
services.AddScoped<ITwoFactorAuthenticationService, TwoFactorAuthenticationService>();
services.AddScoped<IExternalAuthenticationService, ExternalAuthenticationService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
using Grand.Business.Authentication.Services;
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.Infrastructure.Configuration;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Grand.Infrastructure.Configuration;
using Grand.SharedKernel.Attributes;
using Grand.SharedKernel.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Security.Claims;
Expand All @@ -19,25 +15,18 @@
namespace Grand.Business.Authentication.Tests.Services;

[TestClass]
public class ApiAuthenticationServiceTests
public class JwtBearerAuthenticationServiceTests
{
private IApiAuthenticationService _authService;
private Mock<ICustomerService> _customerService;
private Mock<IGroupService> _groupService;
private Mock<IHttpContextAccessor> _httpContextAccessorMoc;
private IJwtBearerAuthenticationService _jwtBearerAuthenticationService;
private Mock<IUserApiService> _userApiServiceMock;
private OidcLoginConfig _oidcLoginConfig;

[TestInitialize]
public void Init()
{
_httpContextAccessorMoc = new Mock<IHttpContextAccessor>();
_customerService = new Mock<ICustomerService>();
_userApiServiceMock = new Mock<IUserApiService>();
_groupService = new Mock<IGroupService>();
_authService = new ApiAuthenticationService(_customerService.Object, _groupService.Object,
_httpContextAccessorMoc.Object, new Mock<IAuthenticationSchemeProvider>().Object);
_oidcLoginConfig = new OidcLoginConfig();
_jwtBearerAuthenticationService =
new JwtBearerAuthenticationService(_customerService.Object, _userApiServiceMock.Object, _oidcLoginConfig);
Expand Down Expand Up @@ -172,7 +161,6 @@ public async Task Valid_ActiveUserApi_ReturnTrue()
Assert.IsTrue(result);
}


[TestMethod]
public async Task Valid_ProviderOnly_ActiveUserApi_ReturnFalse()
{
Expand All @@ -195,105 +183,4 @@ public async Task Valid_ProviderOnly_ActiveUserApi_ReturnFalse()
Assert.AreEqual("Sign-in with API users is disabled: use access tokens from the identity provider",
await _jwtBearerAuthenticationService.ErrorMessage());
}

[TestMethod]
public async Task GetAuthenticatedCustomer_NullAuthHeader_GetNull()
{
var customer = new Customer { Email = "johny@gmail.com" };
var httpContext = new Mock<HttpContext>();
var req = new Mock<HttpRequest>();
req.Setup(c => c.Path).Returns(new PathString("/api/..."));
req.Setup(c => c.Headers).Returns(new HeaderDictionary());
httpContext.Setup(c => c.Request).Returns(req.Object);
_httpContextAccessorMoc.Setup(c => c.HttpContext).Returns(httpContext.Object);
_customerService.Setup(c => c.GetCustomerBySystemName(It.IsAny<string>()))
.Returns(() => Task.FromResult(customer));
var result = await _authService.GetAuthenticatedCustomer();
Assert.IsNull(result);
}

private static HttpContext CreateHttpContext(Action<AuthenticationBuilder> configure, Endpoint endpoint = null)
{
var services = new ServiceCollection();
services.AddLogging();
var builder = services.AddAuthentication();
configure?.Invoke(builder);
var context = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() };
context.Request.Headers["Authorization"] = "Bearer anything";
if (endpoint != null) context.SetEndpoint(endpoint);
return context;
}

private IApiAuthenticationService CreateService(HttpContext context)
{
_httpContextAccessorMoc.Setup(c => c.HttpContext).Returns(context);
return new ApiAuthenticationService(_customerService.Object, _groupService.Object,
_httpContextAccessorMoc.Object,
context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>());
}

[TestMethod]
public async Task GetAuthenticatedCustomer_BearerHeaderWithoutJwtScheme_ReturnsNull()
{
var context = CreateHttpContext(null);
var result = await CreateService(context).GetAuthenticatedCustomer();
Assert.IsNull(result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_FrontApiEndpointWithoutFrontScheme_ReturnsNull()
{
var endpoint = new Endpoint(null,
new EndpointMetadataCollection(new ApiGroupAttribute(ApiConstants.ApiGroupNameV2)), "front");
var context = CreateHttpContext(null, endpoint);
var result = await CreateService(context).GetAuthenticatedCustomer();
Assert.IsNull(result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_JwtSchemeRegisteredAndSucceeds_ReturnsCustomer()
{
var customer = new Customer { Id = "c1", Active = true };
_customerService.Setup(c => c.GetCustomerById("c1")).ReturnsAsync(customer);
_groupService.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true);
var context = CreateHttpContext(b => b.AddScheme<AuthenticationSchemeOptions, OkHandler>(
JwtBearerDefaults.AuthenticationScheme, null));
var result = await CreateService(context).GetAuthenticatedCustomer();
Assert.AreSame(customer, result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_BearerForwardedToOidc_ReturnsNull()
{
//a provider token the Bearer scheme forwards to the Oidc scheme: its CustomerId claim was not issued by GrandNode
var customer = new Customer { Id = "c1", Active = true };
_customerService.Setup(c => c.GetCustomerById("c1")).ReturnsAsync(customer);
_groupService.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true);
var context = CreateHttpContext(b => b
.AddScheme<AuthenticationSchemeOptions, OkHandler>(JwtBearerDefaults.AuthenticationScheme,
o => o.ForwardDefault = "Oidc")
.AddScheme<AuthenticationSchemeOptions, OkHandler>("Oidc", null));

var result = await CreateService(context).GetAuthenticatedCustomer();

Assert.IsNull(result);
_customerService.Verify(c => c.GetCustomerById(It.IsAny<string>()), Times.Never);
_customerService.Verify(c => c.GetCustomerByEmail(It.IsAny<string>()), Times.Never);
}

private class OkHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public OkHandler(Microsoft.Extensions.Options.IOptionsMonitor<AuthenticationSchemeOptions> options,
Microsoft.Extensions.Logging.ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder)
: base(options, logger, encoder)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity(new[] { new Claim("CustomerId", "c1") }, "test");
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name)));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Grand.Business.Core.Interfaces.Customers;
using Grand.Domain.Customers;
using Grand.Web.Common.RequestCustomerResolvers;
using Grand.Web.Common.Security.Authentication;
using Grand.Web.Common.UserAgent;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using Grand.Business.Core.Interfaces.Common.Directory;
using Grand.Business.Core.Interfaces.Customers;
using Grand.Domain.Customers;
using Grand.SharedKernel.Attributes;
using Grand.SharedKernel.Extensions;
using Grand.Web.Common.Security.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Security.Claims;

namespace Grand.Web.Common.Tests.Security.Authentication;

[TestClass]
public class ApiAuthenticationServiceTests
{
private IApiAuthenticationService _authService;
private Mock<ICustomerService> _customerService;
private Mock<IGroupService> _groupService;
private Mock<IHttpContextAccessor> _httpContextAccessorMoc;

[TestInitialize]
public void Init()
{
_httpContextAccessorMoc = new Mock<IHttpContextAccessor>();
_customerService = new Mock<ICustomerService>();
_groupService = new Mock<IGroupService>();
_authService = new ApiAuthenticationService(_customerService.Object, _groupService.Object,
_httpContextAccessorMoc.Object, new Mock<IAuthenticationSchemeProvider>().Object);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_NullAuthHeader_GetNull()
{
var customer = new Customer { Email = "johny@gmail.com" };
var httpContext = new Mock<HttpContext>();
var req = new Mock<HttpRequest>();
req.Setup(c => c.Path).Returns(new PathString("/api/..."));
req.Setup(c => c.Headers).Returns(new HeaderDictionary());
httpContext.Setup(c => c.Request).Returns(req.Object);
_httpContextAccessorMoc.Setup(c => c.HttpContext).Returns(httpContext.Object);
_customerService.Setup(c => c.GetCustomerBySystemName(It.IsAny<string>()))
.Returns(() => Task.FromResult(customer));
var result = await _authService.GetAuthenticatedCustomer();
Assert.IsNull(result);
}

private static HttpContext CreateHttpContext(Action<AuthenticationBuilder> configure, Endpoint endpoint = null)
{
var services = new ServiceCollection();
services.AddLogging();
var builder = services.AddAuthentication();
configure?.Invoke(builder);
var context = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() };
context.Request.Headers["Authorization"] = "Bearer anything";
if (endpoint != null) context.SetEndpoint(endpoint);
return context;
}

private IApiAuthenticationService CreateService(HttpContext context)
{
_httpContextAccessorMoc.Setup(c => c.HttpContext).Returns(context);
return new ApiAuthenticationService(_customerService.Object, _groupService.Object,
_httpContextAccessorMoc.Object,
context.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>());
}

[TestMethod]
public async Task GetAuthenticatedCustomer_BearerHeaderWithoutJwtScheme_ReturnsNull()
{
var context = CreateHttpContext(null);
var result = await CreateService(context).GetAuthenticatedCustomer();
Assert.IsNull(result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_FrontApiEndpointWithoutFrontScheme_ReturnsNull()
{
var endpoint = new Endpoint(null,
new EndpointMetadataCollection(new ApiGroupAttribute(ApiConstants.ApiGroupNameV2)), "front");
var context = CreateHttpContext(null, endpoint);
var result = await CreateService(context).GetAuthenticatedCustomer();
Assert.IsNull(result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_JwtSchemeRegisteredAndSucceeds_ReturnsCustomer()
{
var customer = new Customer { Id = "c1", Active = true };
_customerService.Setup(c => c.GetCustomerById("c1")).ReturnsAsync(customer);
_groupService.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true);
var context = CreateHttpContext(b => b.AddScheme<AuthenticationSchemeOptions, OkHandler>(
JwtBearerDefaults.AuthenticationScheme, null));
var result = await CreateService(context).GetAuthenticatedCustomer();
Assert.AreSame(customer, result);
}

[TestMethod]
public async Task GetAuthenticatedCustomer_BearerForwardedToOidc_ReturnsNull()
{
//a provider token the Bearer scheme forwards to the Oidc scheme: its CustomerId claim was not issued by GrandNode
var customer = new Customer { Id = "c1", Active = true };
_customerService.Setup(c => c.GetCustomerById("c1")).ReturnsAsync(customer);
_groupService.Setup(g => g.IsRegistered(customer)).ReturnsAsync(true);
var context = CreateHttpContext(b => b
.AddScheme<AuthenticationSchemeOptions, OkHandler>(JwtBearerDefaults.AuthenticationScheme,
o => o.ForwardDefault = "Oidc")
.AddScheme<AuthenticationSchemeOptions, OkHandler>("Oidc", null));

var result = await CreateService(context).GetAuthenticatedCustomer();

Assert.IsNull(result);
_customerService.Verify(c => c.GetCustomerById(It.IsAny<string>()), Times.Never);
_customerService.Verify(c => c.GetCustomerByEmail(It.IsAny<string>()), Times.Never);
}

private class OkHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public OkHandler(Microsoft.Extensions.Options.IOptionsMonitor<AuthenticationSchemeOptions> options,
Microsoft.Extensions.Logging.ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder)
: base(options, logger, encoder)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity(new[] { new Claim("CustomerId", "c1") }, "test");
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name)));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
using Grand.Business.Authentication.Services;
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Business.Core.Interfaces.Common.Directory;
using Grand.Business.Core.Interfaces.Customers;
using Grand.Business.Core.Utilities.Authentication;
using Grand.Domain.Common;
using Grand.Domain.Customers;
using Grand.Infrastructure.Configuration;
using Grand.Web.Common.Security.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Security.Claims;

namespace Grand.Business.Authentication.Tests.Services;
namespace Grand.Web.Common.Tests.Security.Authentication;

[TestClass]
public class CookieAuthenticationServiceTests
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Grand.Business.Authentication.Services;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Configuration;
using Grand.Web.Common.Security.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Grand.Business.Authentication.Tests.Services;
namespace Grand.Web.Common.Tests.Security.Authentication;

[TestClass]
public class CookieOptionsFactoryTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Grand.Domain.Tax;
using Grand.Infrastructure.Configuration;
using Grand.Web.Common.RequestCustomerResolvers;
using Grand.Web.Common.Security.Authentication;
using Grand.Web.Common.Security.Oidc;
using Grand.Web.Common.UserAgent;
using Microsoft.AspNetCore.Authentication;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Grand.Domain.Tax;
using Grand.Infrastructure.Configuration;
using Grand.Web.Common.RequestCustomerResolvers;
using Grand.Web.Common.Security.Authentication;
using Grand.Web.Common.UserAgent;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -458,8 +459,8 @@
];
CollectionAssert.AreEqual(values.OrderBy(x => x).ToArray(), values);
Assert.AreEqual(values.Length, values.Distinct().Count());
Assert.AreEqual(1000, RequestCustomerResolverOrder.AllowAnonymous);

Check warning on line 462 in src/Tests/Grand.Web.Common.Tests/WorkContextSetterRequestResolverTests.cs

View workflow job for this annotation

GitHub Actions / build

Review or remove the assertion as its condition is known to be always true (https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0032)
Assert.AreEqual(5000, RequestCustomerResolverOrder.ApiUser);

Check warning on line 463 in src/Tests/Grand.Web.Common.Tests/WorkContextSetterRequestResolverTests.cs

View workflow job for this annotation

GitHub Actions / build

Review or remove the assertion as its condition is known to be always true (https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0032)
}

private class TestableSetter(
Expand Down
4 changes: 2 additions & 2 deletions src/Web/Grand.Web.Admin/Infrastructure/AdminThemeContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Domain.Stores;
using Grand.Domain.Stores;
using Grand.Web.Admin.Extensions;
using Grand.Web.Common.Security.Authentication;
using Grand.Web.Common.Themes;

namespace Grand.Web.Admin.Infrastructure;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Grand.Business.Core.Interfaces.Authentication;
using Grand.Business.Core.Interfaces.Customers;
using Grand.Domain.Customers;
using Grand.Web.Common.Security.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;

Expand Down
Original file line number Diff line number Diff line change
@@ -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.Customers;
using Grand.Domain.Customers;
using Grand.Infrastructure.Configuration;
Expand All @@ -11,7 +10,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;

namespace Grand.Business.Authentication.Services;
namespace Grand.Web.Common.Security.Authentication;

public class ApiAuthenticationService : IApiAuthenticationService
{
Expand Down
Loading
Loading