fix(auth): fix SQLite DateTimeOffset comparison crash in trusted-device checks
The sessions unit's live smoke test caught the same bug it fixed in SessionsController also present in TrustedDeviceService and TwoFactorController's device list: SQLite/Pomelo's EF Core provider cannot translate DateTimeOffset relational comparisons or ORDER BY to SQL, so IsDeviceTrustedAsync (the check that skips 2FA for a trusted browser) and ListTrustedDevices would 500 on real SQLite despite passing on EF's InMemory test provider. Same fix: equality-only in the DB query, expiry comparison and sort after materializing.
This commit is contained in:
@@ -222,13 +222,14 @@ public sealed class TwoFactorController : ControllerBase
|
||||
if (user is null) return Unauthorized();
|
||||
|
||||
var currentHash = TrustedDeviceService.CurrentDeviceTokenHash(Request);
|
||||
// SQLite/Pomelo cannot translate DateTimeOffset ORDER BY to SQL (same issue as the
|
||||
// expiry check in TrustedDeviceService), so sort after materializing.
|
||||
var devices = await _db.TrustedDevices
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.OrderByDescending(x => x.LastSeenAtUtc)
|
||||
.Select(x => new TrustedDeviceDto(x.Id, x.DeviceLabel, x.CreatedAtUtc, x.LastSeenAtUtc, x.ExpiresAtUtc, currentHash != null && x.TokenHash == currentHash))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(devices);
|
||||
return Ok(devices.OrderByDescending(x => x.LastSeenAtUtc).ToList());
|
||||
}
|
||||
|
||||
[HttpDelete("trusted-devices/{id:int}")]
|
||||
|
||||
@@ -26,10 +26,13 @@ public static class TrustedDeviceService
|
||||
|
||||
var hash = HashToken(token);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
// SQLite/Pomelo cannot translate DateTimeOffset relational comparisons (>) to SQL, so the
|
||||
// expiry check has to happen after materializing the row -- fine here since the equality
|
||||
// filters (UserId, TokenHash) already narrow this to at most one row.
|
||||
var match = await db.TrustedDevices
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(x => x.UserId == userId && x.TokenHash == hash && x.ExpiresAtUtc > now, cancellationToken);
|
||||
if (match is null) return false;
|
||||
.FirstOrDefaultAsync(x => x.UserId == userId && x.TokenHash == hash, cancellationToken);
|
||||
if (match is null || match.ExpiresAtUtc <= now) return false;
|
||||
|
||||
match.LastSeenAtUtc = now;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user