10000 fix access token expiration ; refactored unit tests · devel0/example-webapp-with-auth@53016a0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 53016a0

Browse files
committed
fix access token expiration ; refactored unit tests
1 parent 777d4a7 commit 53016a0

32 files changed

+1394
-1111
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
<noWarn>1591</noWarn>
44
<GenerateDocumentationFile>true</GenerateDocumentationFile>
55

6-
<MSCoreVersion>9.0.3</MSCoreVersion>
6+
<MSCoreVersion>8.0.7</MSCoreVersion>
77
</PropertyGroup>
88
</Project>

src/backend/Constants/Constants.Web.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public static partial class Constants
1111
public const string WEB_CookieName_XAccessToken = "X-Access-Token";
1212
public const string WEB_CookieName_XRefreshToken = "X-Refresh-Token";
1313

14+
public const string WEB_CookieName_XCSRFToken = "X-CSRF-TOKEN";
15+
1416
public const string WEB_HeadersCollection_SetCookie = "Set-Cookie";
1517

1618
}

src/backend/Controllers/AuthController.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,35 @@ public async Task<ActionResult<CurrentUserResponseDto>> CurrentUser()
5454
throw new NotImplementedException($"{nameof(CurrentUserResponseDto)}.{nameof(CurrentUserResponseDto.Status)} == {res.Status}");
5555
}
5656
}
57+
58+
[HttpPost]
59+
public async Task<ActionResult<RenewAccessTokenResponse>> RenewAccessToken()
60+
{
61+
var res = await authService.RenewCurrentUserAccessTokenAsync(cancellationToken);
62+
63+
switch (res.Status)
64+
{
65+
case RenewAccessTokenStatus.OK:
66+
return res;
67+
68+
case RenewAccessTokenStatus.InvalidAuthentication:
69+
case RenewAccessTokenStatus.InvalidAccessToken:
70+
case RenewAccessTokenStatus.InvalidRefreshToken:
71+
return Unauthorized();
72+
73+
case RenewAccessTokenStatus.InvalidHttpContext:
74+
return BadRequest();
75+
76+
default:
77+
throw new NotImplementedException($"{nameof(RenewAccessTokenStatus)} == {res}");
78+
}
79+
}
5780

5881
/// <summary>
5982
/// Renew refresh token of current user if refresh token still valid.
6083
/// This is used to extends refresh token duration avoiding closing frontend session.
6184
/// </summary>
62-
[HttpGet]
85+
[HttpPost]
6386
public async Task<ActionResult<RenewRefreshTokenResponse>> RenewRefreshToken()
6487
{
6588
var res = await authService.RenewCurrentUserRefreshTokenAsync(cancellationToken);

src/backend/Controllers/MainController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ ILogger<MainController> logger
2323
/// <summary>
2424
/// Long running api test.
2525
/// </summary>
26-
[HttpGet]
26+
[HttpGet]
2727
[Authorize(Roles = ROLE_admin)]
2828
public async Task<ActionResult> LongRunning()
2929
{

src/backend/Extensions/Auth.cs

Lines changed: 32 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -145,73 +145,50 @@ public static void SetupJWTAuthentication(this WebApplicationBuilder builder) =>
145145
{
146146
options.TokenValidationParameters = builder.Configuration.GetTokenVaildationParameters();
147147

148-
if (options.Events == null) options.Events = new JwtBearerEvents();
149-
150-
options.Events.OnAuthenticationFailed = async context =>
148+
options.Events = new JwtBearerEvents
151149
{
152-
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException) &&
153-
context.HttpContext is not null)
150+
151+
OnMessageReceived = async context =>
154152
{
155-
// access token expired
153+
var jwtService = context.HttpContext.RequestServices.GetRequiredService<IJWTService>();
154+
155+
if (context.Request.Cookies.ContainsKey(WEB_CookieName_XAccessToken))
156+
{
157+
var accessToken = context.Request.Cookies[WEB_CookieName_XAccessToken];
158+
159+
160+
var decoded = jwtService.DecodeAccessToken(accessToken);
161+
162+
if (accessToken is not null && jwtService.IsAccessTokenValid(accessToken))
163+
{
164+
context.Token = accessToken;
165+
166+
return;
167+
}
168+
169+
}
170+
171+
var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
172+
var hostEnvironment = context.HttpContext.RequestServices.GetRequiredService<IHostEnvironment>();
173+
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<AuthController>>();
174+
var cancellationToken = context.HttpContext.RequestServices.GetRequiredService<CancellationToken>();
156175

157-
var jwtService = context.HttpContext.RequestServices.GetService<IJWTService>();
158-
var userManager = context.HttpContext.RequestServices.GetService<UserManager<ApplicationUser>>();
159-
var hostEnvironment = context.HttpContext.RequestServices.GetService<IHostEnvironment>();
160-
var logger = context.HttpContext.RequestServices.GetService<ILogger<AuthController>>();
161-
var cancellationToken = context.HttpContext.RequestServices.GetService<CancellationToken>();
176+
logger.LogTrace($"no access token provided");
162177

163-
var accessToken = context.HttpContext.Request.Cookies[WEB_CookieName_XAccessToken];
164178
F438 var refreshToken = context.HttpContext.Request.Cookies[WEB_CookieName_XRefreshToken];
165179

166-
if (jwtService is not null && hostEnvironment is not null &&
167-
userManager is not null && logger is not null &&
168-
!string.IsNullOrEmpty(accessToken) && !string.IsNullOrEmpty(refreshToken))
180+
if (!string.IsNullOrEmpty(refreshToken))
169181
{
170-
var principal = jwtService.GetPrincipalFromExpiredToken(accessToken);
171-
var username = principal?.GetUserInfoFromClaims().UserName;
182+
var accessTokenNfo = await jwtService.RenewAccessTokenAsync(refreshToken, cancellationToken);
172183

173-
if (username is not null &&
174-
jwtService.IsRefreshTokenStillValid(username, refreshToken))
184+
if (accessTokenNfo is not null)
175185
{
176-
var user = await userManager.FindByNameAsync(username);
177-
var dtNow = DateTime.UtcNow;
178-
var userIsLockedOut = user is not null && await userManager.IsLockedOutAsync(user);
179-
var userDisabled = user is not null && user.Disabled == true;
180-
181-
if (user is not null && !userIsLockedOut && !userDisabled)
182-
{
183-
var res = await jwtService.RenewAccessTokenAsync(accessToken, refreshToken, cancellationToken);
184-
185-
if (res is not null)
186-
{
187-
var opts = new CookieOptions();
188-
189-
context.HttpContext.Response.Cookies.Append(
190-
WEB_CookieName_XAccessToken,
191-
res.AccessToken,
192-
new CookieOptions
193-
{
194-
Secure = true,
195-
HttpOnly = true,
196-
SameSite = SameSiteMode.Strict,
197-
Expires = DateTimeOffset.UtcNow + builder.Configuration.GetAppConfig().Auth.Jwt.AccessTokenDuration
198-
});
199-
200-
context.Principal = res.Principal;
201-
context.Success();
202-
}
203-
}
186+
jwtService.AddAccessTokenToHttpResponse(context.HttpContext.Response, accessTokenNfo);
187+
context.Token = accessTokenNfo.AccessToken;
204188
}
205189
}
206-
}
207-
};
208-
209-
options.Events.OnMessageReceived = context =>
210-
{
211-
if (context.Request.Cookies.ContainsKey(WEB_CookieName_XAccessToken))
212-
context.Token = context.Request.Cookies[WEB_CookieName_XAccessToken];
213190

214-
return Task.CompletedTask;
191+
}
215192
};
216193

217194
});

src/backend/Extensions/DatabaseService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public static void ConfigureDatabase(this WebApplicationBuilder webApplicationBu
3737
{
3838
var migrationAssembly = Assembly.GetAssembly(typeof(AppDbContext));
3939
if (migrationAssembly is null) throw new Exception($"couldn't find migration assembly");
40-
options.MigrationsAssembly(migrationAssembly);
40+
options.MigrationsAssembly(migrationAssembly.FullName);
4141
});
4242
}
4343
break;

src/backend/Extensions/Swagger.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public static void ConfigSwagger(this WebApplication webApplication)
6969

7070
webApplication.UseSwagger(c =>
7171
{
72-
c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0;
72+
//c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0;
7373
});
7474
webApplication.UseSwaggerUI(c =>
7575
{

src/backend/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
app.ConfigApis();
9797

9898
// add endpoints controller related
99-
app.MapControllers();
99+
app.MapControllers();
100100

101101
// auto apply database pending migrations
102102
await app.ApplyDatabaseMigrations(cts.Token);

src/backend/Services/Abstractions/Auth/DTOs/LoginResponseDto.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ public class LoginResponseDto
5959
/// <summary>
6060
/// Permissions related to this user roles.
6161
/// </summary>
62-
public HashSet<UserPermission> Permissions { get; set; } = new();
62+
public HashSet<UserPermission> Permissions { get; set; } = new();
6363

6464
/// <summary>
6565
/// Expiration timestamp for the refresh token. To keep alive auth issue <see cref="AuthController.RenewRefreshToken"/> before
66-
/// refresh token expire.
66+
/// token expire.
6767
/// </summary>
6868
public DateTimeOffset RefreshTokenExpiration { get; set; }
6969

src/backend/Services/Abstractions/Auth/DTOs/RenewAccessTokenNfo.cs

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
namespace ExampleWebApp.Backend.WebApi.Services.Abstractions.Auth.DTOs;
2+
3+
/// <summary>
4+
/// <see cref="AuthController.RenewAccessToken"/> response api specific status.
5+
/// </summary>
6+
public enum RenewAccessTokenStatus
7+
{
8+
/// <summary>
9+
/// Valid refresh token, thus access token renewd.
10+
/// </summary>
11+
OK,
12+
13+
/// <summary>
14+
/// Invalid authentication.
15+
/// </summary>
16+
InvalidAuthentication,
17+
18+
/// <summary>
19+
/// Invalid access token.
20+
/// </summary>
21+
InvalidAccessToken,
22+
23+
/// <summary>
24+
/// Invalid refresh token.
25+
/// </summary>
26+
InvalidRefreshToken,
27+
28+
/// <summary>
29+
/// Invalid http context.
30+
/// </summary>
31+
InvalidHttpContext,
32+
}
33+
34+
/// <summary>
35+
/// <see cref="AuthController.RenewAccessToken"/> api response data.
36+
/// </summary>
37+
public class RenewAccessTokenResponse
38+
{
39+
40+
/// <summary>
41+
/// API specific status response.
42+
/// </summary>
43+
[Required]
44+
public required RenewAccessTokenStatus Status { get; set; }
45+
46+
public AccessTokenNfo? AccessTokenNfo { get; set; }
47+
48+
}

src/backend/Services/Abstractions/Auth/IAuthService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Task<LoginResponseDto> LoginAsync(
2828
Task<CurrentUserResponseDto> CurrentUserNfoAsync(
2929
CancellationToken cancellationToken);
3030

31+
/// <summary>
32+
/// Renew access token if valid refresh token was found
33+
/// </summary>
34+
Task<RenewAccessTokenResponse> RenewCurrentUserAccessTokenAsync(CancellationToken cancellationToken);
35+
3136
/// <summary>
3237
/// Renew refresh token of current user if refresh token still valid.
3338
/// This is used to extends refresh token duration avoiding closing frontend session.

0 commit comments

Comments
 (0)
0