Skip to content

Commit 75d75a6

Browse files
Improvements/code (#584)
* Refactor cookie options creation with ICookieOptionsFactory * Refactor nested if statements for readability * Add environment-specific CORS policies * Remove SonarQube workflow and add SonarCloud properties configuration * Revert (#579) - Update ContactController
1 parent 0af58ae commit 75d75a6

16 files changed

Lines changed: 203 additions & 94 deletions

File tree

.github/workflows/sonarcube.yml

Lines changed: 0 additions & 49 deletions
This file was deleted.

.sonarcloud.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sonar.coverage.exclusions="**" `
2+
sonar.cpd.exclusions="**"
3+
sonar.language=cs
4+
sonar.exclusions=**/*.js,**/*.css,**/*.html,**/*.ts

src/Business/Grand.Business.Authentication/Services/CookieAuthenticationService.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ public CookieAuthenticationService(
3131
ICustomerService customerService,
3232
IGroupService groupService,
3333
IHttpContextAccessor httpContextAccessor,
34+
ICookieOptionsFactory cookieOptionsFactory,
3435
SecurityConfig securityConfig)
3536
{
3637
_customerSettings = customerSettings;
3738
_customerService = customerService;
3839
_groupService = groupService;
3940
_httpContextAccessor = httpContextAccessor;
41+
_cookieOptionsFactory = cookieOptionsFactory;
4042
_securityConfig = securityConfig;
4143
}
4244

@@ -54,6 +56,8 @@ public CookieAuthenticationService(
5456
private readonly ICustomerService _customerService;
5557
private readonly IGroupService _groupService;
5658
private readonly IHttpContextAccessor _httpContextAccessor;
59+
private readonly ICookieOptionsFactory _cookieOptionsFactory;
60+
5761
private readonly SecurityConfig _securityConfig;
5862

5963
#endregion
@@ -224,10 +228,7 @@ public virtual Task SetCustomerGuid(Guid customerGuid)
224228
return Task.CompletedTask;
225229

226230
//set new cookie value
227-
var options = new CookieOptions {
228-
HttpOnly = true,
229-
Expires = cookieExpiresDate
230-
};
231+
var options = _cookieOptionsFactory.Create(cookieExpiresDate);
231232
_httpContextAccessor.HttpContext.Response.Cookies.Append(CustomerCookieName, customerGuid.ToString(), options);
232233

233234
return Task.CompletedTask;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Grand.Business.Core.Interfaces.Authentication;
2+
using Grand.Infrastructure.Configuration;
3+
using Microsoft.AspNetCore.Http;
4+
5+
namespace Grand.Business.Authentication.Services;
6+
7+
/// <summary>
8+
/// Factory for creating secure cookie options
9+
/// </summary>
10+
public class CookieOptionsFactory(SecurityConfig securityConfig) : ICookieOptionsFactory
11+
{
12+
public string CookiePrefix => securityConfig.CookiePrefix;
13+
14+
/// <summary>
15+
/// Creates cookie options with proper security settings
16+
/// </summary>
17+
/// <param name="expiresDate">Optional explicit expiration date</param>
18+
/// <returns>Configured cookie options</returns>
19+
public CookieOptions Create(DateTime? expiresDate = null)
20+
{
21+
var cookieExpiresDate = expiresDate ?? DateTime.UtcNow.AddHours(securityConfig.CookieAuthExpires);
22+
23+
var options = new CookieOptions {
24+
HttpOnly = true,
25+
Expires = cookieExpiresDate,
26+
Secure = securityConfig.CookieSecurePolicyAlways,
27+
SameSite = securityConfig.CookieSameSite,
28+
};
29+
30+
return options;
31+
}
32+
}

src/Business/Grand.Business.Authentication/Startup/StartupApplication.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class StartupApplication : IStartupApplication
1313
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
1414
{
1515
services.AddScoped<IGrandAuthenticationService, CookieAuthenticationService>();
16+
services.AddScoped<ICookieOptionsFactory, CookieOptionsFactory>();
1617
services.AddScoped<IApiAuthenticationService, ApiAuthenticationService>();
1718
services.AddScoped<IJwtBearerAuthenticationService, JwtBearerAuthenticationService>();
1819
services.AddScoped<ITwoFactorAuthenticationService, TwoFactorAuthenticationService>();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace Grand.Business.Core.Interfaces.Authentication;
4+
5+
// <summary>
6+
/// Interface for factory creating secure cookie options
7+
/// </summary>
8+
public interface ICookieOptionsFactory
9+
{
10+
/// <summary>
11+
/// Creates cookie options with proper security settings
12+
/// </summary>
13+
/// <param name="expiresDate">Optional explicit expiration date</param>
14+
/// <returns>Configured cookie options</returns>
15+
CookieOptions Create(DateTime? expiresDate = null);
16+
17+
/// <summary>
18+
/// Gets the cookie prefix
19+
/// </summary>
20+
string CookiePrefix { get; }
21+
}

src/Modules/Grand.Module.Api/Constants/Configurations.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
public static class Configurations
44
{
55
public const string RestRoutePrefix = "api";
6-
public const string CorsPolicyName = "CorsPolicy";
6+
public const string DevelopmentCorsPolicyName = "DevelopmentCorsPolicy";
7+
public const string ProductionCorsPolicyName = "ProductionCorsPolicy";
78
public const int MaxLimit = 100;
89
}

src/Modules/Grand.Module.Api/Infrastructure/CorsStartup.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,31 @@
44
using Microsoft.AspNetCore.Hosting;
55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Hosting;
78

89
namespace Grand.Module.Api.Infrastructure;
910

1011
public class CorsStartup : IStartupApplication
1112
{
1213
public void Configure(WebApplication application, IWebHostEnvironment webHostEnvironment)
1314
{
14-
application.UseCors(Configurations.CorsPolicyName);
15+
if(webHostEnvironment.IsDevelopment())
16+
application.UseCors(Configurations.DevelopmentCorsPolicyName);
17+
else
18+
application.UseCors(Configurations.ProductionCorsPolicyName);
1519
}
1620

1721
public void ConfigureServices(IServiceCollection services,
1822
IConfiguration configuration)
1923
{
24+
var allowedOrigins = configuration.GetSection("AllowedHostOrigins").Get<string[]>() ?? Array.Empty<string>();
25+
2026
services.AddCors(options =>
2127
{
22-
options.AddPolicy(Configurations.CorsPolicyName,
28+
options.AddPolicy(Configurations.DevelopmentCorsPolicyName,
2329
builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
30+
options.AddPolicy(Configurations.ProductionCorsPolicyName,
31+
builder => builder.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader());
2432
});
2533
}
2634

src/Tests/Grand.Business.Authentication.Tests/Services/CookieAuthenticationServiceTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Grand.Business.Authentication.Services;
2+
using Grand.Business.Core.Interfaces.Authentication;
23
using Grand.Business.Core.Interfaces.Common.Directory;
34
using Grand.Business.Core.Interfaces.Customers;
45
using Grand.Business.Core.Utilities.Authentication;
@@ -24,6 +25,7 @@ public class CookieAuthenticationServiceTests
2425
private Mock<IHttpContextAccessor> _httpAccessorMock;
2526
private DefaultHttpContext _httpContext;
2627
private Mock<IServiceProvider> serviceProviderMock;
28+
private CookieOptionsFactory _cookieOptionsFactory;
2729

2830
[TestInitialize]
2931
public void Init()
@@ -37,8 +39,9 @@ public void Init()
3739
CookieClaimsIssuer = "grandnode",
3840
CookiePrefix = ".Grand."
3941
};
42+
_cookieOptionsFactory = new CookieOptionsFactory(_config);
4043
_cookieAuthService = new CookieAuthenticationService(_customerSettings, _customerServiceMock.Object,
41-
_groupServiceMock.Object, _httpAccessorMock.Object, _config);
44+
_groupServiceMock.Object, _httpAccessorMock.Object, _cookieOptionsFactory, _config);
4245
//For mock HttpContext extension methods like SignOutAsync ,SignInAsync etc..
4346
_authServiceMock = new Mock<IAuthenticationService>();
4447
serviceProviderMock = new Mock<IServiceProvider>();
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Grand.Business.Authentication.Services;
2+
using Grand.Infrastructure.Configuration;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
6+
namespace Grand.Business.Authentication.Tests.Services;
7+
8+
[TestClass]
9+
public class CookieOptionsFactoryTests
10+
{
11+
private SecurityConfig _securityConfig;
12+
private CookieOptionsFactory _cookieOptionsFactory;
13+
14+
[TestInitialize]
15+
public void Setup()
16+
{
17+
_securityConfig = new SecurityConfig {
18+
CookieAuthExpires = 24,
19+
CookieSecurePolicyAlways = true,
20+
};
21+
_cookieOptionsFactory = new CookieOptionsFactory(_securityConfig);
22+
}
23+
24+
[TestMethod]
25+
public void Create_WhenCalledWithoutExpiryDate_SetsDefaultExpiryDate()
26+
{
27+
// Arrange
28+
var expectedExpiryDate = DateTime.UtcNow.AddHours(_securityConfig.CookieAuthExpires);
29+
30+
// Act
31+
var options = _cookieOptionsFactory.Create();
32+
33+
// Assert
34+
Assert.IsTrue(options.HttpOnly);
35+
Assert.IsTrue(options.Secure);
36+
37+
// Verify expiration is close to expected (allowing for slight processing time differences)
38+
var difference = (options.Expires.Value - expectedExpiryDate).TotalMinutes;
39+
Assert.IsTrue(Math.Abs(difference) < 1, "Expiry time should be within 1 minute of expected value");
40+
}
41+
42+
[TestMethod]
43+
public void Create_WhenCalledWithExpiryDate_UsesProvidedExpiryDate()
44+
{
45+
// Arrange
46+
var expiryDate = DateTime.UtcNow.AddDays(7);
47+
48+
// Act
49+
var options = _cookieOptionsFactory.Create(expiryDate);
50+
51+
// Assert
52+
Assert.IsTrue(options.HttpOnly);
53+
Assert.IsTrue(options.Secure);
54+
Assert.AreEqual(expiryDate, options.Expires);
55+
}
56+
57+
[TestMethod]
58+
public void Create_WhenSecurePolicyIsNone_SecureFlagIsFalse()
59+
{
60+
// Arrange
61+
_securityConfig.CookieSecurePolicyAlways = true;
62+
var factory = new CookieOptionsFactory(_securityConfig);
63+
64+
// Act
65+
var options = factory.Create();
66+
67+
// Assert
68+
Assert.IsTrue(options.Secure);
69+
}
70+
71+
[TestMethod]
72+
public void Create_WhenSecurePolicyIsSameAsRequest_UsesConfiguredPolicy()
73+
{
74+
// Arrange
75+
_securityConfig.CookieSecurePolicyAlways = false;
76+
var factory = new CookieOptionsFactory(_securityConfig);
77+
78+
// Act
79+
var options = factory.Create();
80+
81+
// Assert
82+
Assert.IsFalse(options.Secure);
83+
}
84+
85+
[TestMethod]
86+
public void Create_WhenSameSiteModeIsStrict_UseStrictMode()
87+
{
88+
// Arrange
89+
_securityConfig.CookieSameSite = SameSiteMode.Strict;
90+
var factory = new CookieOptionsFactory(_securityConfig);
91+
92+
// Act
93+
var options = factory.Create();
94+
95+
// Assert
96+
Assert.AreEqual(SameSiteMode.Strict, options.SameSite);
97+
}
98+
}

0 commit comments

Comments
 (0)