Skip to content

Commit 6f73802

Browse files
committed
JWT Token code now handles multiple logins from one user
1 parent e87e33a commit 6f73802

5 files changed

Lines changed: 102 additions & 30 deletions

File tree

AuthPermissions.AspNetCore/Services/DisableJwtRefreshToken.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
namespace AuthPermissions.AspNetCore.Services
1111
{
1212
/// <summary>
13-
/// This service allows you to mark the Jwt Refresh Token as 'used' so that the JWT token cannot be refreshed
13+
/// This service allows you to mark the Jwt Refresh Token as 'used' so that the JWT token cannot be refreshed.
14+
/// There are two methods: one to allow a user to logout and another to allow a admin person log out all logins of a specific user.
1415
/// </summary>
1516
public class DisableJwtRefreshToken : IDisableJwtRefreshToken
1617
{
@@ -26,16 +27,16 @@ public DisableJwtRefreshToken(AuthPermissionsDbContext context)
2627
}
2728

2829
/// <summary>
29-
/// This will mark the latest, valid RefreshToken as invalid.
30-
/// Call this a) when a user logs out, or b) you want to log out an active user when the JTW times out
30+
/// This allows a user to logout. The effect is that when the JWT Token expires
31+
/// you cannot refresh the JWT Token because the refresh token is invalid.
32+
/// If the user has multiple logins, this only logs out the login using the given refresh token.
3133
/// </summary>
32-
/// <param name="userId"></param>
33-
public async Task MarkJwtRefreshTokenAsUsedAsync(string userId)
34+
/// <param name="refreshToken"></param>
35+
/// <returns></returns>
36+
public async Task LogoutUserViaRefreshTokenAsync(string refreshToken)
3437
{
3538
var latestValidRefreshToken = await _context.RefreshTokens
36-
.Where(x => x.UserId == userId && !x.IsInvalid)
37-
.OrderByDescending(x => x.AddedDateUtc)
38-
.FirstOrDefaultAsync();
39+
.SingleOrDefaultAsync(x => x.TokenValue == refreshToken);
3940

4041
if (latestValidRefreshToken != null)
4142
{
@@ -44,5 +45,22 @@ public async Task MarkJwtRefreshTokenAsUsedAsync(string userId)
4445
status.IfErrorsTurnToException();
4546
}
4647
}
48+
49+
/// <summary>
50+
/// This will mark all the refresh tokens linked to this userid will be marked as invalid,
51+
/// which means the user cannot refresh the JWT Token. If the user has multiple logins,
52+
/// then all the users logins will marked as a invalid.
53+
/// </summary>
54+
/// <param name="userId"></param>
55+
public async Task LogoutUserViaUserIdAsync(string userId)
56+
{
57+
var latestValidRefreshTokens = await _context.RefreshTokens
58+
.Where(x => x.UserId == userId && !x.IsInvalid)
59+
.ToListAsync();
60+
61+
latestValidRefreshTokens.ForEach(x => x.MarkAsInvalid());
62+
var status = await _context.SaveChangesWithChecksAsync();
63+
status.IfErrorsTurnToException();
64+
}
4765
}
4866
}
Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1-
// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
1+
// Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
22
// Licensed under MIT license. See License.txt in the project root for license information.
33

44
using System.Threading.Tasks;
55

6-
namespace AuthPermissions.AspNetCore.Services
6+
namespace AuthPermissions.AspNetCore.Services;
7+
8+
/// <summary>
9+
/// This service allows you to mark the Jwt Refresh Token as 'used' so that the JWT token cannot be refreshed.
10+
/// There are two methods: one to allow a user to logout and another to allow a admin person log out all logins of a specific user.
11+
/// </summary>
12+
public interface IDisableJwtRefreshToken
713
{
814
/// <summary>
9-
/// Service to disable the current JWT Refresh Token
15+
/// This allows a user to logout. The effect is that when the JWT Token expires
16+
/// you cannot refresh the JWT Token because the refresh token is invalid.
17+
/// If the user has multiple logins, this only logs out the login using the given refresh token.
18+
/// </summary>
19+
/// <param name="refreshToken"></param>
20+
/// <returns></returns>
21+
Task LogoutUserViaRefreshTokenAsync(string refreshToken);
22+
23+
/// <summary>
24+
/// This will mark all the refresh tokens linked to this userid will be marked as invalid,
25+
/// which means the user cannot refresh the JWT Token. If the user has multiple logins,
26+
/// then all the users logins will marked as a invalid.
1027
/// </summary>
11-
public interface IDisableJwtRefreshToken
12-
{
13-
/// <summary>
14-
/// This will mark the latest, valid RefreshToken as invalid.
15-
/// Call this a) when a user logs out, or b) you want to log out an active user when the JTW times out
16-
/// </summary>
17-
/// <param name="userId"></param>
18-
Task MarkJwtRefreshTokenAsUsedAsync(string userId);
19-
}
28+
/// <param name="userId"></param>
29+
Task LogoutUserViaUserIdAsync(string userId);
2030
}

Example2.WebApiWithToken.IndividualAccounts/Controllers/AuthenticateController.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,15 @@ public async Task<ActionResult<TokenAndRefreshToken>> RefreshAuthentication(Toke
112112
}
113113

114114
/// <summary>
115-
/// This will mark the JST refresh as used, so the user cannot refresh the JWT Token
115+
/// This will mark the JST refresh as used, so the user cannot refresh the JWT Token.
116116
/// </summary>
117117
/// <returns></returns>
118118
[Authorize]
119119
[HttpPost]
120120
[Route("logout")]
121-
public async Task<ActionResult> Logout([FromServices]IDisableJwtRefreshToken service)
121+
public async Task<ActionResult> Logout([FromServices]IDisableJwtRefreshToken service, string refreshToken)
122122
{
123-
var userId = User.GetUserIdFromUser();
124-
await service.MarkJwtRefreshTokenAsUsedAsync(userId);
123+
await service.LogoutUserViaRefreshTokenAsync(refreshToken);
125124

126125
return Ok();
127126
}

ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 3.5.0
44

5+
- BREAKING CHANGE (small): The DisableJwtRefreshToken service has been updated to handle multiple logins from one user
56
- BREAKING CHANGE (small): Changed TenantChangeCookieEvent name to SomethingChangedCookieEvent
67
- Improved feature: AuthPermissionsDbContext now takes mutiple IDatabaseStateChangeEvent
78
- Improved feature: No AuthP database event change listeners will be triggered during bulk loading

Test/UnitTests/TestAuthPermissions/TestTokenBuilderAndRefresh.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public async Task RefreshTokenUsingRefreshTokenAsyncRefreshHasExpired()
209209
}
210210

211211
[Fact]
212-
public async Task TestMarkJwtRefreshTokenAsUsedAsync()
212+
public async Task TestLogoutUserViaRefreshTokenAsync_TwoSameUsers()
213213
{
214214
//SETUP
215215
var options = SqliteInMemory.CreateOptions<AuthPermissionsDbContext>();
@@ -218,22 +218,66 @@ public async Task TestMarkJwtRefreshTokenAsUsedAsync()
218218

219219
var setup = new SetupTokenBuilder(context);
220220
await setup.TokenBuilder.GenerateTokenAndRefreshTokenAsync("User1");
221+
await setup.TokenBuilder.GenerateTokenAndRefreshTokenAsync("User1");
222+
223+
var refreshTokensSet = context.RefreshTokens.OrderBy(x => x.AddedDateUtc);
221224

222-
var beforeToken = context.RefreshTokens.Single();
223-
beforeToken.IsInvalid.ShouldBeFalse();
225+
var firstTokenBefore = refreshTokensSet.First();
226+
firstTokenBefore.IsInvalid.ShouldBeFalse();
227+
228+
var lastTokenBefore = refreshTokensSet.Last();
229+
lastTokenBefore.IsInvalid.ShouldBeFalse();
224230

225231
context.ChangeTracker.Clear();
226232
var service = new DisableJwtRefreshToken(context);
227233

228234
//ATTEMPT
229-
await service.MarkJwtRefreshTokenAsUsedAsync("User1");
235+
await service.LogoutUserViaRefreshTokenAsync(firstTokenBefore.TokenValue);
230236

231237
//VERIFY
232238
context.ChangeTracker.Clear();
233-
var afterToken = context.RefreshTokens.Single();
234-
afterToken.IsInvalid.ShouldBeTrue();
239+
240+
var firstTokenAfter = refreshTokensSet.First();
241+
firstTokenAfter.IsInvalid.ShouldBeTrue();
242+
243+
var lastTokenAfter = refreshTokensSet.Last();
244+
lastTokenAfter.IsInvalid.ShouldBeFalse();
235245
}
236246

247+
[Fact]
248+
public async Task LogoutUserViaUserIdAsync_TwoSameUsers()
249+
{
250+
//SETUP
251+
var options = SqliteInMemory.CreateOptions<AuthPermissionsDbContext>();
252+
using var context = new AuthPermissionsDbContext(options);
253+
context.Database.EnsureCreated();
254+
255+
var setup = new SetupTokenBuilder(context);
256+
await setup.TokenBuilder.GenerateTokenAndRefreshTokenAsync("User1");
257+
await setup.TokenBuilder.GenerateTokenAndRefreshTokenAsync("User1");
258+
259+
var refreshTokensSet = context.RefreshTokens.OrderBy(x => x.AddedDateUtc);
260+
261+
var firstTokenBefore = refreshTokensSet.First();
262+
firstTokenBefore.IsInvalid.ShouldBeFalse();
263+
264+
var lastTokenBefore = refreshTokensSet.Last();
265+
lastTokenBefore.IsInvalid.ShouldBeFalse();
266+
267+
context.ChangeTracker.Clear();
268+
var service = new DisableJwtRefreshToken(context);
269+
270+
//ATTEMPT
271+
await service.LogoutUserViaUserIdAsync("User1");
237272

273+
//VERIFY
274+
context.ChangeTracker.Clear();
275+
276+
var firstTokenAfter = refreshTokensSet.First();
277+
firstTokenAfter.IsInvalid.ShouldBeTrue();
278+
279+
var lastTokenAfter = refreshTokensSet.Last();
280+
lastTokenAfter.IsInvalid.ShouldBeTrue();
281+
}
238282
}
239283
}

0 commit comments

Comments
 (0)