diff --git a/coffeecard/CoffeeCard.Library/Migrations/20241001160733_AddTokenProperties.Designer.cs b/coffeecard/CoffeeCard.Library/Migrations/20241022153731_AddTokenProperties.Designer.cs
similarity index 99%
rename from coffeecard/CoffeeCard.Library/Migrations/20241001160733_AddTokenProperties.Designer.cs
rename to coffeecard/CoffeeCard.Library/Migrations/20241022153731_AddTokenProperties.Designer.cs
index 749c3140..e6e5f7e1 100644
--- a/coffeecard/CoffeeCard.Library/Migrations/20241001160733_AddTokenProperties.Designer.cs
+++ b/coffeecard/CoffeeCard.Library/Migrations/20241022153731_AddTokenProperties.Designer.cs
@@ -12,7 +12,7 @@
namespace CoffeeCard.Library.Migrations
{
[DbContext(typeof(CoffeeCardContext))]
- [Migration("20241001160733_AddTokenProperties")]
+ [Migration("20241022153731_AddTokenProperties")]
partial class AddTokenProperties
{
///
@@ -312,9 +312,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
b.Property("Expires")
.HasColumnType("datetime2");
- b.Property("PreviousTokenId")
- .HasColumnType("int");
-
b.Property("Revoked")
.HasColumnType("bit");
diff --git a/coffeecard/CoffeeCard.Library/Migrations/20241001160733_AddTokenProperties.cs b/coffeecard/CoffeeCard.Library/Migrations/20241022153731_AddTokenProperties.cs
similarity index 82%
rename from coffeecard/CoffeeCard.Library/Migrations/20241001160733_AddTokenProperties.cs
rename to coffeecard/CoffeeCard.Library/Migrations/20241022153731_AddTokenProperties.cs
index 4003e7b0..86117337 100644
--- a/coffeecard/CoffeeCard.Library/Migrations/20241001160733_AddTokenProperties.cs
+++ b/coffeecard/CoffeeCard.Library/Migrations/20241022153731_AddTokenProperties.cs
@@ -19,13 +19,6 @@ protected override void Up(MigrationBuilder migrationBuilder)
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
- migrationBuilder.AddColumn(
- name: "PreviousTokenId",
- schema: "dbo",
- table: "Tokens",
- type: "int",
- nullable: true);
-
migrationBuilder.AddColumn(
name: "Revoked",
schema: "dbo",
@@ -51,11 +44,6 @@ protected override void Down(MigrationBuilder migrationBuilder)
schema: "dbo",
table: "Tokens");
- migrationBuilder.DropColumn(
- name: "PreviousTokenId",
- schema: "dbo",
- table: "Tokens");
-
migrationBuilder.DropColumn(
name: "Revoked",
schema: "dbo",
diff --git a/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs b/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs
index 4b0a942a..0a96efb5 100644
--- a/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs
+++ b/coffeecard/CoffeeCard.Library/Migrations/CoffeeCardContextModelSnapshot.cs
@@ -309,9 +309,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("Expires")
.HasColumnType("datetime2");
- b.Property("PreviousTokenId")
- .HasColumnType("int");
-
b.Property("Revoked")
.HasColumnType("bit");
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs
index 31015704..7b0b7e5c 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/AccountService.cs
@@ -320,11 +320,8 @@ public async Task SendMagicLinkEmail(string email, LoginType loginType)
public async Task LoginByMagicLink(string token)
{
// Validate token in DB
- var foundToken = await GetTokenByMagicLink(token);
- if (foundToken.Revoked)
- {
- throw new ApiException("Token already used", 401);
- }
+ var foundToken = await _tokenServiceV2.GetValidTokenByHashAsync(token);
+
// Invalidate token in DB
foundToken.Revoked = true;
await _context.SaveChangesAsync();
@@ -347,12 +344,8 @@ public async Task LoginByMagicLink(string token)
public async Task RefreshToken(string token)
{
- var foundToken = await GetRefreshToken(token);
- if (foundToken.Revoked)
- {
- await InvalidateTokenChain(foundToken.Id);
- throw new ApiException("Token already used", 401);
- }
+ var foundToken = await _tokenServiceV2.GetValidTokenByHashAsync(token);
+
// Invalidate token in DB
foundToken.Revoked = true;
await _context.SaveChangesAsync();
@@ -372,42 +365,5 @@ public async Task RefreshToken(string token)
return new UserLoginResponse() { Jwt = jwt, RefreshToken = refreshToken };
}
-
- private async Task GetRefreshToken(string token)
- {
- var foundToken = await _context.Tokens
- .Include(t => t.User)
- .FirstOrDefaultAsync(t => t.TokenHash == token);
- if (foundToken?.User == null)
- {
- throw new ApiException("Invalid token", 401);
- }
-
- return foundToken;
- }
-
- private async Task GetTokenByMagicLink(string token)
- {
- var foundToken = await _context.Tokens
- .Include(t => t.User)
- .FirstOrDefaultAsync(t => t.TokenHash == token);
- if (foundToken?.User == null)
- {
- throw new ApiException("Invalid token", 401);
- }
-
- return foundToken;
- }
-
- private async Task InvalidateTokenChain(int tokenId)
- {
- // todo: invalidate all from user instead of recursion
- var newerToken = _context.Tokens.FirstOrDefault(t => t.PreviousTokenId == tokenId);
- if (newerToken != null)
- {
- newerToken.Revoked = true;
- await InvalidateTokenChain(newerToken.Id);
- }
- }
}
}
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ITokenService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ITokenService.cs
index be91cb0a..dec5f858 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/ITokenService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/ITokenService.cs
@@ -8,5 +8,6 @@ public interface ITokenService
string GenerateMagicLink(User user);
Task GenerateRefreshTokenAsync(User user);
Task ValidateTokenAsync(string token);
+ Task GetValidTokenByHashAsync(string tokenHash);
}
}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/TokenService.cs b/coffeecard/CoffeeCard.Library/Services/v2/TokenService.cs
index b9e674d1..23ff00a7 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/TokenService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/TokenService.cs
@@ -32,7 +32,7 @@ public string GenerateMagicLink(User user)
public async Task GenerateRefreshTokenAsync(User user)
{
var refreshToken = Guid.NewGuid().ToString();
- _context.Tokens.Add(new Token(refreshToken, TokenType.Refresh));
+ _context.Tokens.Add(new Token(refreshToken, TokenType.Refresh) { User = user });
await _context.SaveChangesAsync();
return refreshToken;
}
@@ -42,9 +42,34 @@ public async Task ValidateTokenAsync(string refreshToken)
var token = await _context.Tokens.FirstOrDefaultAsync(t => t.TokenHash == refreshToken);
if (token.Revoked)
{
- // TODO: Invalidate chain of tokens
+ await InvalidateRefreshTokensForUser(token.User);
throw new ApiException("Refresh token is already used", 401);
}
throw new NotImplementedException();
}
+
+ public async Task GetValidTokenByHashAsync(string tokenHash)
+ {
+ var foundToken = await _context.Tokens.Include(t => t.User).FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
+ if (foundToken == null || foundToken.Revoked || foundToken.Expired())
+ {
+ await InvalidateRefreshTokensForUser(foundToken?.User);
+ throw new ApiException("Invalid token", 401);
+ }
+ return foundToken;
+ }
+
+ private async Task InvalidateRefreshTokensForUser(User user)
+ {
+ if (user is null) return;
+
+ var tokens = _context.Tokens.Where(t => t.UserId == user.Id && t.Type == TokenType.Refresh);
+
+ _context.Tokens.UpdateRange(tokens);
+ foreach (var token in tokens)
+ {
+ token.Revoked = true;
+ }
+ await _context.SaveChangesAsync();
+ }
}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Models/Entities/Token.cs b/coffeecard/CoffeeCard.Models/Entities/Token.cs
index 08f627ed..15878c44 100644
--- a/coffeecard/CoffeeCard.Models/Entities/Token.cs
+++ b/coffeecard/CoffeeCard.Models/Entities/Token.cs
@@ -20,8 +20,6 @@ public class Token(string tokenHash, TokenType type)
public bool Revoked { get; set; } = false;
- public int? PreviousTokenId { get; set; }
-
public override bool Equals(object? obj)
{
if (obj is Token newToken) return TokenHash.Equals(newToken.TokenHash);
@@ -32,5 +30,10 @@ public override int GetHashCode()
{
return HashCode.Combine(Id, TokenHash, User);
}
+
+ public bool Expired()
+ {
+ return DateTime.UtcNow > Expires;
+ }
}
}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs
index 768ffad4..ba5b6fdc 100644
--- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs
+++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/AccountController.cs
@@ -258,12 +258,23 @@ public async Task> AuthToken([FromRoute] string
[HttpPost]
[AuthorizeRoles(UserGroup.Customer, UserGroup.Barista, UserGroup.Manager, UserGroup.Board)]
- [Route("auth/refresh")]
- public async Task> Refresh()
+ [Route("auth/refresh/loginType={loginType}")]
+ public async Task> Refresh([FromRoute] LoginType loginType, string refreshToken = null)
{
- var refreshToken = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "refreshToken").Value;
+ switch (loginType)
+ {
+ case LoginType.App:
+ if (refreshToken is null) return NotFound(new MessageResponseDto { Message = "Refresh token required for app refresh." });
+ break;
+ case LoginType.Shifty:
+ refreshToken = HttpContext.Request.Cookies.FirstOrDefault(c => c.Key == "refreshToken").Value;
+ break;
+ default:
+ return NotFound(new MessageResponseDto { Message = "Cannot determine application to login." });
+ }
+
var token = await _accountService.RefreshToken(refreshToken);
- return Ok(token);
+ return Tokenize(loginType, token);
}
private ActionResult Tokenize(LoginType loginType, UserLoginResponse token)