Skip to content

Commit a2de1c5

Browse files
committed
2 parents b7e5a46 + b37d2cb commit a2de1c5

24 files changed

Lines changed: 155 additions & 76 deletions

File tree

Sources/EasyExtensions.AspNetCore.Authorization/Controllers/BaseAuthController.cs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,7 @@ public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto?
129129
var roles = await GetUserRolesAsync(userId.Value);
130130
string accessToken = CreateAccessToken(userId.Value, roles);
131131
await SaveAndRevokeRefreshTokenAsync(userId.Value, request.RefreshToken, newRefreshToken, AuthType.Unknown);
132-
Response.Cookies.Append(CookieRefreshTokenName, newRefreshToken, new()
133-
{
134-
Secure = true,
135-
HttpOnly = true,
136-
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,
137-
Expires = DateTimeOffset.UtcNow.Add(GetCookieExpirationTime()),
138-
});
132+
AddRefreshTokenToCookie(newRefreshToken);
139133
return Ok(new TokenPairResponseDto
140134
{
141135
AccessToken = accessToken,
@@ -159,7 +153,7 @@ public async Task<IActionResult> Login([FromBody] LoginRequestDto request)
159153
Guid? userId = await FindUserByUsernameAsync(request.Username);
160154
if (!userId.HasValue || userId == Guid.Empty)
161155
{
162-
await OnUserLoggingInAsync(userId.Value, AuthType.Credentials, AuthRejectionType.UserNotFound);
156+
await OnUserLoggingInAsync(userId.GetValueOrDefault(), AuthType.Credentials, AuthRejectionType.UserNotFound);
163157
return this.ApiUnauthorized("Invalid username or password");
164158
}
165159
bool canLogin = await CanUserLoginAsync(userId.Value);
@@ -185,13 +179,7 @@ public async Task<IActionResult> Login([FromBody] LoginRequestDto request)
185179
string refreshToken = StringHelpers.CreateRandomString(64);
186180
await SaveAndRevokeRefreshTokenAsync(userId.Value, string.Empty, refreshToken, AuthType.Credentials);
187181
await OnUserLoggingInAsync(userId.Value, AuthType.Credentials, AuthRejectionType.None);
188-
Response.Cookies.Append(CookieRefreshTokenName, refreshToken, new()
189-
{
190-
Secure = true,
191-
HttpOnly = true,
192-
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,
193-
Expires = DateTimeOffset.UtcNow.Add(GetCookieExpirationTime()),
194-
});
182+
AddRefreshTokenToCookie(refreshToken);
195183
return Ok(new TokenPairResponseDto
196184
{
197185
AccessToken = accessToken,
@@ -241,13 +229,7 @@ public async Task<IActionResult> LoginWithGoogle([FromQuery] string token)
241229
string refreshToken = StringHelpers.CreateRandomString(64);
242230
await SaveAndRevokeRefreshTokenAsync(userId.Value, string.Empty, refreshToken, AuthType.Google);
243231
await OnUserLoggingInAsync(userId.Value, AuthType.Google, AuthRejectionType.None);
244-
Response.Cookies.Append(CookieRefreshTokenName, refreshToken, new()
245-
{
246-
Secure = true,
247-
HttpOnly = true,
248-
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,
249-
Expires = DateTimeOffset.UtcNow.Add(GetCookieExpirationTime()),
250-
});
232+
AddRefreshTokenToCookie(refreshToken);
251233
return Ok(new TokenPairResponseDto
252234
{
253235
AccessToken = accessToken,
@@ -390,7 +372,18 @@ public virtual IEnumerable<KeyValuePair<string, string>> GetAdditionalTokenClaim
390372
return [];
391373
}
392374

393-
private string CreateAccessToken(Guid userId, IEnumerable<string> roles)
375+
/// <summary>
376+
/// Generates a JWT access token for the specified user, including their roles and any additional claims.
377+
/// </summary>
378+
/// <remarks>The generated token includes standard claims such as the subject identifier, as well
379+
/// as any additional claims retrieved for the user. Roles are added as claims to support role-based
380+
/// authorization scenarios.</remarks>
381+
/// <param name="userId">The unique identifier of the user for whom the access token is being generated.</param>
382+
/// <param name="roles">A collection of role names to be included as claims in the access token. Each role represents a permission
383+
/// or group associated with the user.</param>
384+
/// <returns>A string containing the generated JWT access token that can be used to authenticate the user in subsequent
385+
/// requests.</returns>
386+
internal protected string CreateAccessToken(Guid userId, IEnumerable<string> roles)
394387
{
395388
return _tokenProvider.CreateToken(cb =>
396389
{
@@ -406,5 +399,24 @@ private string CreateAccessToken(Guid userId, IEnumerable<string> roles)
406399
return cb;
407400
});
408401
}
402+
403+
/// <summary>
404+
/// Adds the specified refresh token to the HTTP response cookies to support secure session renewal.
405+
/// </summary>
406+
/// <remarks>The refresh token cookie is configured with security best practices: it is marked as
407+
/// secure, HTTP-only, and uses a strict SameSite policy to help prevent cross-site request forgery (CSRF)
408+
/// attacks. The cookie's expiration is determined by the application's configured refresh token
409+
/// lifetime.</remarks>
410+
/// <param name="refreshToken">The refresh token to be stored in the response cookie. Cannot be null or empty.</param>
411+
internal protected void AddRefreshTokenToCookie(string refreshToken)
412+
{
413+
Response.Cookies.Append(CookieRefreshTokenName, refreshToken, new()
414+
{
415+
Secure = true,
416+
HttpOnly = true,
417+
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,
418+
Expires = DateTimeOffset.UtcNow.Add(GetCookieExpirationTime()),
419+
});
420+
}
409421
}
410422
}

Sources/EasyExtensions.AspNetCore.Authorization/EasyExtensions.AspNetCore.Authorization.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="LICENSE.md" />
3232
<ProjectReference Include="..\EasyExtensions.AspNetCore\EasyExtensions.AspNetCore.csproj" />
3333
<ProjectReference Include="..\EasyExtensions\EasyExtensions.csproj" />
34-
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" />
35-
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
36-
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
37-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
34+
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="10.0.4" />
35+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" />
36+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.4" />
37+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.104" PrivateAssets="All" />
3838
</ItemGroup>
3939

4040
</Project>

Sources/EasyExtensions.AspNetCore.Authorization/Extensions/ServiceCollectionExtensions.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ namespace EasyExtensions.AspNetCore.Authorization.Extensions
2121
/// </summary>
2222
public static class ServiceCollectionExtensions
2323
{
24+
/// <summary>
25+
/// Represents the name of the parameter used to pass the access token.
26+
/// </summary>
27+
public const string AccessTokenParamName = "access_token";
28+
2429
/// <summary>
2530
/// Adds JWT authentication resolving <see cref="IConfiguration"/> from DI.
2631
/// Reads settings from JwtSettings section or flat fallback Jwt[Key] configuration values (see <see cref="ConfigurationExtensions.GetJwtSettings"/>).
2732
/// </summary>
2833
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
34+
/// <param name="useCookies">If <c>true</c>, JWT token will be read from cookies (if not found in query string).</param>
2935
/// <returns>Current <see cref="IServiceCollection"/> instance.</returns>
3036
/// <exception cref="KeyNotFoundException">When required JWT settings are missing.</exception>
3137
/// <remarks>
@@ -62,7 +68,7 @@ public static class ServiceCollectionExtensions
6268
/// <item><description><c>RequireHttpsMetadata = false</c> (adjust in production if needed).</description></item>
6369
/// </list>
6470
/// </remarks>
65-
public static IServiceCollection AddJwt(this IServiceCollection services)
71+
public static IServiceCollection AddJwt(this IServiceCollection services, bool useCookies = false)
6672
{
6773
services.AddScoped<ITokenProvider, JwtTokenProvider>();
6874
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
@@ -92,11 +98,19 @@ public static IServiceCollection AddJwt(this IServiceCollection services)
9298
{
9399
OnMessageReceived = context =>
94100
{
95-
var accessToken = context.Request.Query["access_token"].ToString();
101+
var accessToken = context.Request.Query[AccessTokenParamName].ToString();
96102
if (!string.IsNullOrEmpty(accessToken))
97103
{
98104
context.Token = accessToken;
99105
}
106+
else if (useCookies && string.IsNullOrWhiteSpace(context.Token))
107+
{
108+
string? cookieToken = context.Request.Cookies[AccessTokenParamName];
109+
if (!string.IsNullOrEmpty(cookieToken))
110+
{
111+
context.Token = cookieToken;
112+
}
113+
}
100114
return Task.CompletedTask;
101115
}
102116
};

Sources/EasyExtensions.AspNetCore.Sentry/EasyExtensions.AspNetCore.Sentry.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
3333
<PackageReference Include="Sentry.AspNetCore" Version="6.1.0" />
3434
<ProjectReference Include="..\EasyExtensions.AspNetCore\EasyExtensions.AspNetCore.csproj" />
35-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
35+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.104" PrivateAssets="All" />
3636
</ItemGroup>
3737

3838
</Project>

Sources/EasyExtensions.AspNetCore.Stack/Builders/EasyStackOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public class EasyStackOptions
2525
/// </summary>
2626
internal bool AuthorizationEnabled { get; set; }
2727

28+
/// <summary>
29+
/// Gets or sets a value indicating whether to use secrets for configuration values.
30+
/// </summary>
31+
internal bool UseSecretVault { get; set; }
32+
2833
/// <summary>
2934
/// Enables authorization for the current EasyStackOptions instance.
3035
/// </summary>
@@ -66,5 +71,17 @@ public EasyStackOptions WithPostgres<TDbContext>(bool useLazyLoadingProxies = fa
6671
};
6772
return this;
6873
}
74+
75+
/// <summary>
76+
/// Configures whether sensitive information should be stored in the secret vault.
77+
/// </summary>
78+
/// <param name="useSecrets">A value indicating whether to enable the use of the secret vault. Set to <see langword="true"/> to use the
79+
/// vault for sensitive information; otherwise, <see langword="false"/>.</param>
80+
/// <returns>The current instance of <see cref="EasyStackOptions"/> to allow for method chaining.</returns>
81+
public EasyStackOptions UseSecrets(bool useSecrets)
82+
{
83+
UseSecretVault = useSecrets;
84+
return this;
85+
}
6986
}
7087
}

Sources/EasyExtensions.AspNetCore.Stack/EasyExtensions.AspNetCore.Stack.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
<ProjectReference Include="..\EasyExtensions.EntityFrameworkCore.Npgsql\EasyExtensions.EntityFrameworkCore.Npgsql.csproj" />
3535
<ProjectReference Include="..\EasyExtensions.EntityFrameworkCore\EasyExtensions.EntityFrameworkCore.csproj" />
3636
<ProjectReference Include="..\EasyExtensions.Quartz\EasyExtensions.Quartz.csproj" />
37-
<PackageReference Include="EasyVault" Version="1.0.10" />
38-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
37+
<PackageReference Include="EasyVault" Version="1.0.11" />
38+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.104" PrivateAssets="All" />
3939
</ItemGroup>
4040

4141
</Project>

Sources/EasyExtensions.AspNetCore.Stack/Extensions/HostApplicationBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public static IHostApplicationBuilder AddEasyStack(
133133

134134
// EasyVault - if URL presented in configuration
135135
bool isVaultPresented = !string.IsNullOrWhiteSpace(builder.Configuration[EasyVault.SDK.Extensions.ConfigurationExtensions.DefaultServerUrlKey]);
136-
if (isVaultPresented)
136+
if (isVaultPresented && options.UseSecretVault)
137137
{
138138
EasyVault.SDK.Extensions.ConfigurationExtensions.AddSecrets(builder.Configuration);
139139
logger.LogInformation("VaultApiUrl found in configuration, added EasyVault");

Sources/EasyExtensions.AspNetCore/EasyExtensions.AspNetCore.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="LICENSE.md" />
3232
<ProjectReference Include="..\EasyExtensions\EasyExtensions.csproj" />
3333
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.9" />
34-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
35-
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.3" />
36-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
34+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.104" PrivateAssets="All" />
35+
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.4" />
36+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
3737
</ItemGroup>
3838

3939
</Project>

Sources/EasyExtensions.Clients/EasyExtensions.Clients.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
3131
<None Include="packageIcon.png" Pack="true" PackagePath="\" />
3232
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="LICENSE.md" />
33-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
34-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
33+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" />
34+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.104" PrivateAssets="All" />
3535
</ItemGroup>
3636

3737
</Project>

Sources/EasyExtensions.Crypto.Tests/EasyExtensions.Crypto.Tests.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="coverlet.collector" Version="6.0.4">
13+
<PackageReference Include="coverlet.collector" Version="8.0.0">
1414
<PrivateAssets>all</PrivateAssets>
1515
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1616
</PackageReference>
17-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
18-
<PackageReference Include="NUnit" Version="4.4.0" />
19-
<PackageReference Include="NUnit.Analyzers" Version="4.11.2">
17+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
18+
<PackageReference Include="NUnit" Version="4.5.1" />
19+
<PackageReference Include="NUnit.Analyzers" Version="4.12.0">
2020
<PrivateAssets>all</PrivateAssets>
2121
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2222
</PackageReference>

0 commit comments

Comments
 (0)