forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -152,10 +152,19 @@ public sealed class AuthService(
|
||||
RefreshSessionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!sessionService.TryParseRefreshToken(request.RefreshToken, out var locator))
|
||||
{
|
||||
throw new SessionRevokedException();
|
||||
}
|
||||
|
||||
var tokenHash = sessionService.HashRefreshToken(request.RefreshToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var session = await dbContext.AuthSessions
|
||||
.SingleOrDefaultAsync(entity => entity.TokenHash == tokenHash, cancellationToken);
|
||||
.SingleOrDefaultAsync(entity =>
|
||||
entity.Id == locator.SessionId &&
|
||||
entity.TenantId == locator.TenantId &&
|
||||
entity.TokenHash == tokenHash,
|
||||
cancellationToken);
|
||||
|
||||
if (session is null || session.RevokedAt is not null || session.ExpiresAt <= now)
|
||||
{
|
||||
@@ -194,9 +203,18 @@ public sealed class AuthService(
|
||||
LogoutSessionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!sessionService.TryParseRefreshToken(request.RefreshToken, out var locator))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenHash = sessionService.HashRefreshToken(request.RefreshToken);
|
||||
var session = await dbContext.AuthSessions
|
||||
.SingleOrDefaultAsync(entity => entity.TokenHash == tokenHash, cancellationToken);
|
||||
.SingleOrDefaultAsync(entity =>
|
||||
entity.Id == locator.SessionId &&
|
||||
entity.TenantId == locator.TenantId &&
|
||||
entity.TokenHash == tokenHash,
|
||||
cancellationToken);
|
||||
|
||||
if (session is null || session.RevokedAt is not null)
|
||||
{
|
||||
|
||||
@@ -16,9 +16,29 @@ public sealed class SessionService(
|
||||
{
|
||||
private readonly JwtOptions options = options.Value;
|
||||
|
||||
public string GenerateRefreshToken()
|
||||
public string GenerateRefreshToken(Guid tenantId, Guid sessionId)
|
||||
{
|
||||
return Base64UrlEncoder.Encode(RandomNumberGenerator.GetBytes(64));
|
||||
return $"v1.{tenantId:N}.{sessionId:N}.{Base64UrlEncoder.Encode(RandomNumberGenerator.GetBytes(64))}";
|
||||
}
|
||||
|
||||
public bool TryParseRefreshToken(string refreshToken, out RefreshTokenLocator locator)
|
||||
{
|
||||
locator = default;
|
||||
if (string.IsNullOrWhiteSpace(refreshToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = refreshToken.Split('.', 4, StringSplitOptions.None);
|
||||
if (parts.Length != 4 || parts[0] != "v1" || parts[3].Length < 32 ||
|
||||
!Guid.TryParseExact(parts[1], "N", out var tenantId) ||
|
||||
!Guid.TryParseExact(parts[2], "N", out var sessionId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
locator = new RefreshTokenLocator(tenantId, sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public string HashRefreshToken(string refreshToken)
|
||||
@@ -37,17 +57,19 @@ public sealed class SessionService(
|
||||
string? userAgent,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var refreshToken = GenerateRefreshToken();
|
||||
var session = new AuthSession
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = membership.TenantId,
|
||||
UserId = userId,
|
||||
TokenHash = HashRefreshToken(refreshToken),
|
||||
TokenHash = string.Empty,
|
||||
Provider = provider,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(options.RefreshTokenDays),
|
||||
IpAddress = ipAddress,
|
||||
UserAgent = userAgent
|
||||
};
|
||||
var refreshToken = GenerateRefreshToken(session.TenantId, session.Id);
|
||||
session.TokenHash = HashRefreshToken(refreshToken);
|
||||
|
||||
dbContext.AuthSessions.Add(session);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user