mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
163 lines
4.0 KiB
C#
163 lines
4.0 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.Admin;
|
|
using RadicadeServer.DB;
|
|
|
|
namespace RadicadeServer.Auth;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static class AdminSession
|
|
{
|
|
public const string CookieName = "radicade_admin_session";
|
|
public const string StateCookieName = "radicade_oauth_state";
|
|
public const string RedirectCookieName = "radicade_oauth_redirect";
|
|
|
|
|
|
public static readonly TimeSpan Lifetime = TimeSpan.FromHours(12);
|
|
|
|
public static readonly TimeSpan AbsoluteLifetime = TimeSpan.FromDays(7);
|
|
|
|
|
|
private static readonly TimeSpan RotationGrace = TimeSpan.FromMinutes(2);
|
|
|
|
|
|
public static async Task<string> CreateAsync(RecDBContext db, long adminUserId)
|
|
{
|
|
var token = NewToken();
|
|
db.AdminSessions.Add(new AdminSessionRecord
|
|
{
|
|
AdminUserId = adminUserId,
|
|
TokenHash = Hash(token),
|
|
CreatedAt = DateTime.UtcNow,
|
|
LastSeenAt = DateTime.UtcNow,
|
|
ExpiresAt = DateTime.UtcNow.Add(Lifetime)
|
|
});
|
|
await db.SaveChangesAsync();
|
|
return token;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static async Task<(bool Ok, long UserId)> ValidateAsync(RecDBContext db, string? token)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token)) return (false, 0);
|
|
|
|
var now = DateTime.UtcNow;
|
|
var hash = Hash(token);
|
|
|
|
var record = await db.AdminSessions.FirstOrDefaultAsync(s => s.TokenHash == hash);
|
|
if (record == null)
|
|
{
|
|
|
|
record = await db.AdminSessions.FirstOrDefaultAsync(s =>
|
|
s.PreviousTokenHash == hash && s.LastSeenAt > now - RotationGrace);
|
|
if (record == null) return (false, 0);
|
|
}
|
|
|
|
if (record.RevokedAt.HasValue || record.ExpiresAt <= now)
|
|
return (false, 0);
|
|
|
|
var ceiling = record.CreatedAt.Add(AbsoluteLifetime);
|
|
if (now >= ceiling) return (false, 0);
|
|
|
|
record.LastSeenAt = now;
|
|
record.ExpiresAt = now.Add(Lifetime) < ceiling ? now.Add(Lifetime) : ceiling;
|
|
await db.SaveChangesAsync();
|
|
|
|
return (true, record.AdminUserId);
|
|
}
|
|
|
|
|
|
|
|
|
|
private static readonly object RotateLock = new();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static (string Token, bool Rotated) TryRotate(RecDBContext db, string? currentToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(currentToken)) return ("", false);
|
|
|
|
lock (RotateLock)
|
|
{
|
|
var record = db.AdminSessions.FirstOrDefault(s => s.TokenHash == Hash(currentToken));
|
|
if (record == null)
|
|
{
|
|
|
|
|
|
|
|
record = db.AdminSessions.FirstOrDefault(s => s.PreviousTokenHash == Hash(currentToken));
|
|
if (record != null && record.LastSeenAt > DateTime.UtcNow - RotationGrace)
|
|
return (currentToken, false);
|
|
return (currentToken, false);
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
var ceiling = record.CreatedAt.Add(AbsoluteLifetime);
|
|
if (now >= ceiling) return (currentToken, false);
|
|
|
|
var next = NewToken();
|
|
record.PreviousTokenHash = record.TokenHash;
|
|
record.TokenHash = Hash(next);
|
|
record.LastSeenAt = now;
|
|
record.ExpiresAt = now.Add(Lifetime) < ceiling ? now.Add(Lifetime) : ceiling;
|
|
db.SaveChanges();
|
|
return (next, true);
|
|
}
|
|
}
|
|
|
|
public static async Task RevokeAsync(RecDBContext db, string? token)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token)) return;
|
|
|
|
var record = await db.AdminSessions.FirstOrDefaultAsync(s => s.TokenHash == Hash(token));
|
|
if (record == null) return;
|
|
|
|
record.RevokedAt = DateTime.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
|
|
public static async Task RevokeAllAsync(RecDBContext db, long adminUserId)
|
|
{
|
|
var sessions = await db.AdminSessions
|
|
.Where(s => s.AdminUserId == adminUserId && !s.RevokedAt.HasValue)
|
|
.ToListAsync();
|
|
foreach (var session in sessions)
|
|
session.RevokedAt = DateTime.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
|
|
public static async Task PruneAsync(RecDBContext db)
|
|
{
|
|
var cutoff = DateTime.UtcNow.AddDays(-2);
|
|
var stale = await db.AdminSessions
|
|
.Where(s => s.RevokedAt.HasValue || s.ExpiresAt < cutoff)
|
|
.ToListAsync();
|
|
if (stale.Count == 0) return;
|
|
db.AdminSessions.RemoveRange(stale);
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private static string NewToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
|
|
|
|
private static string Hash(string token) =>
|
|
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
|
|
}
|