Skip to content

Commit ad116bb

Browse files
committed
(Security) Fixed a critical security issue in the authentication service
1 parent afab8c6 commit ad116bb

File tree

5 files changed

+131
-45
lines changed

5 files changed

+131
-45
lines changed

OpenBioCardServer/Models/Entities/Token.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public class Token
2020
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
2121
public DateTime LastUsed { get; set; } = DateTime.UtcNow;
2222

23+
// Token过期时间(默认7天)
24+
public DateTime ExpiresAt { get; set; } = DateTime.UtcNow.AddDays(7);
25+
2326
/// <summary>
2427
/// 设备信息(可选),用于标识设备
2528
/// </summary>
@@ -28,4 +31,7 @@ public class Token
2831

2932
// Navigation property
3033
public Account Account { get; set; } = null!;
34+
35+
// 检查Token是否过期
36+
public bool IsExpired() => DateTime.UtcNow > ExpiresAt;
3137
}

OpenBioCardServer/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ await context.HttpContext.Response.WriteAsJsonAsync(new
9797
// Services
9898
builder.Services.AddScoped<ClassicAuthService>();
9999
builder.Services.AddScoped<AuthService>();
100+
builder.Services.AddHostedService<TokenCleanupService>();
100101

101102
var app = builder.Build();
102103

OpenBioCardServer/Services/AuthService.cs

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,20 @@ public AuthService(
2727
if (string.IsNullOrWhiteSpace(token))
2828
return (false, null);
2929

30-
// Check if it's a root token
31-
if (token.StartsWith("root-"))
32-
{
33-
var rootAccount = await GetRootAccountAsync();
34-
return (rootAccount != null, rootAccount);
35-
}
36-
3730
var tokenEntity = await _context.Tokens
3831
.Include(t => t.Account)
3932
.FirstOrDefaultAsync(t => t.TokenValue == token);
4033

4134
if (tokenEntity == null)
4235
return (false, null);
36+
37+
if (tokenEntity.IsExpired())
38+
{
39+
_context.Tokens.Remove(tokenEntity);
40+
await _context.SaveChangesAsync();
41+
return (false, null);
42+
}
4343

44-
// Update last used timestamp
4544
tokenEntity.LastUsed = DateTime.UtcNow;
4645
await _context.SaveChangesAsync();
4746

@@ -59,25 +58,41 @@ public AuthService(
5958

6059
public async Task<string> CreateTokenAsync(Account account)
6160
{
62-
string tokenValue = account.Type == UserType.Root
63-
? $"root-{Guid.NewGuid()}"
64-
: Guid.NewGuid().ToString();
65-
66-
if (account.Type != UserType.Root)
61+
const int maxTokensPerAccount = 10;
62+
63+
var existingTokenCount = await _context.Tokens
64+
.CountAsync(t => t.AccountId == account.Id && t.ExpiresAt > DateTime.UtcNow);
65+
66+
if (existingTokenCount >= maxTokensPerAccount)
6767
{
68-
var token = new Token
68+
var oldestToken = await _context.Tokens
69+
.Where(t => t.AccountId == account.Id)
70+
.OrderBy(t => t.CreatedAt)
71+
.FirstOrDefaultAsync();
72+
73+
if (oldestToken != null)
6974
{
70-
TokenValue = tokenValue,
71-
AccountId = account.Id
72-
};
73-
74-
_context.Tokens.Add(token);
75-
await _context.SaveChangesAsync();
75+
_context.Tokens.Remove(oldestToken);
76+
}
7677
}
7778

79+
var tokenValue = Guid.NewGuid().ToString();
80+
81+
var token = new Token
82+
{
83+
TokenValue = tokenValue,
84+
AccountId = account.Id,
85+
DeviceInfo = account.Type == UserType.Root ? "Root Login" : null,
86+
ExpiresAt = DateTime.UtcNow.AddDays(7)
87+
};
88+
89+
_context.Tokens.Add(token);
90+
await _context.SaveChangesAsync();
91+
7892
return tokenValue;
7993
}
8094

95+
8196
public async Task<bool> HasAdminPermissionAsync(Account account) =>
8297
account.Type == UserType.Admin || account.Type == UserType.Root;
8398

OpenBioCardServer/Services/ClassicAuthService.cs

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,20 @@ public ClassicAuthService(
2323

2424
public async Task<(bool isValid, Account? account)> ValidateTokenAsync(string token)
2525
{
26-
// Check if it's a root token
27-
if (token.StartsWith("root-"))
28-
{
29-
// For root, we don't store tokens in DB, just verify format and return root account
30-
var rootAccount = await GetRootAccountAsync();
31-
return (rootAccount != null, rootAccount);
32-
}
33-
3426
var tokenEntity = await _context.Tokens
3527
.Include(t => t.Account)
3628
.FirstOrDefaultAsync(t => t.TokenValue == token);
3729

3830
if (tokenEntity == null)
3931
return (false, null);
32+
33+
if (tokenEntity.IsExpired())
34+
{
35+
_context.Tokens.Remove(tokenEntity);
36+
await _context.SaveChangesAsync();
37+
return (false, null);
38+
}
4039

41-
// Update last used timestamp
4240
tokenEntity.LastUsed = DateTime.UtcNow;
4341
await _context.SaveChangesAsync();
4442

@@ -57,26 +55,36 @@ public ClassicAuthService(
5755

5856
public async Task<string> CreateTokenAsync(Account account)
5957
{
60-
string tokenValue;
61-
62-
if (account.Type == UserType.Root)
58+
const int maxTokensPerAccount = 10;
59+
60+
var existingTokenCount = await _context.Tokens
61+
.CountAsync(t => t.AccountId == account.Id && t.ExpiresAt > DateTime.UtcNow);
62+
63+
if (existingTokenCount >= maxTokensPerAccount)
6364
{
64-
// Root tokens have special format and are not stored in DB
65-
tokenValue = $"root-{Guid.NewGuid()}";
65+
var oldestToken = await _context.Tokens
66+
.Where(t => t.AccountId == account.Id)
67+
.OrderBy(t => t.CreatedAt)
68+
.FirstOrDefaultAsync();
69+
70+
if (oldestToken != null)
71+
{
72+
_context.Tokens.Remove(oldestToken);
73+
}
6674
}
67-
else
75+
76+
var tokenValue = Guid.NewGuid().ToString();
77+
78+
var token = new Token
6879
{
69-
tokenValue = Guid.NewGuid().ToString();
70-
71-
var token = new Token
72-
{
73-
TokenValue = tokenValue,
74-
AccountId = account.Id
75-
};
80+
TokenValue = tokenValue,
81+
AccountId = account.Id,
82+
DeviceInfo = account.Type == UserType.Root ? "Root Login" : null,
83+
ExpiresAt = DateTime.UtcNow.AddDays(7)
84+
};
7685

77-
_context.Tokens.Add(token);
78-
await _context.SaveChangesAsync();
79-
}
86+
_context.Tokens.Add(token);
87+
await _context.SaveChangesAsync();
8088

8189
return tokenValue;
8290
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using OpenBioCardServer.Data;
3+
4+
namespace OpenBioCardServer.Services;
5+
6+
public class TokenCleanupService : BackgroundService
7+
{
8+
private readonly IServiceProvider _services;
9+
private readonly ILogger<TokenCleanupService> _logger;
10+
private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(1);
11+
12+
public TokenCleanupService(
13+
IServiceProvider services,
14+
ILogger<TokenCleanupService> logger)
15+
{
16+
_services = services;
17+
_logger = logger;
18+
}
19+
20+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
21+
{
22+
_logger.LogInformation("Token cleanup service started");
23+
24+
while (!stoppingToken.IsCancellationRequested)
25+
{
26+
try
27+
{
28+
await CleanupExpiredTokensAsync();
29+
}
30+
catch (Exception ex)
31+
{
32+
_logger.LogError(ex, "Error cleaning expired tokens");
33+
}
34+
35+
await Task.Delay(_cleanupInterval, stoppingToken);
36+
}
37+
}
38+
39+
private async Task CleanupExpiredTokensAsync()
40+
{
41+
using var scope = _services.CreateScope();
42+
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
43+
44+
var expiredTokens = await context.Tokens
45+
.Where(t => t.ExpiresAt < DateTime.UtcNow)
46+
.ToListAsync();
47+
48+
if (expiredTokens.Any())
49+
{
50+
context.Tokens.RemoveRange(expiredTokens);
51+
await context.SaveChangesAsync();
52+
53+
_logger.LogInformation("Cleared {{Count}} expired tokens", expiredTokens.Count);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)