Files
jobtrackingapp/JobTrackerApi/Services/TwoFactorPendingTokenService.cs
T

48 lines
2.0 KiB
C#

using System.Security.Cryptography;
using Microsoft.Extensions.Caching.Memory;
namespace JobTrackerApi.Services;
public sealed record PendingTwoFactorSession(string UserId, bool RememberMe, string? SecurityStamp);
public interface ITwoFactorPendingTokenService
{
string IssuePendingToken(string userId, bool rememberMe, string? securityStamp = null);
PendingTwoFactorSession? Resolve(string pendingToken, bool consume);
}
// ponytail: server-side opaque token in IMemoryCache, deliberately NOT a JWT. A JWT signed
// with the app's normal signing key would be accepted by the "local" JWT bearer auth scheme
// for every other endpoint unless its issuer/audience/claims were carefully kept out of that
// scheme's validation -- an opaque cache-backed token can never be presented as a bearer
// token, so it structurally cannot grant a real session by itself. Single instance is fine:
// this is a short-lived (5 min), single-process dev/prod deployment, same as the rest of this
// app's in-memory state (rate limiter, IMemoryCache already registered in Program.cs).
public sealed class TwoFactorPendingTokenService : ITwoFactorPendingTokenService
{
private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(5);
private readonly IMemoryCache _cache;
public TwoFactorPendingTokenService(IMemoryCache cache)
{
_cache = cache;
}
public string IssuePendingToken(string userId, bool rememberMe, string? securityStamp = null)
{
var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant();
_cache.Set(CacheKey(token), new PendingTwoFactorSession(userId, rememberMe, securityStamp), Ttl);
return token;
}
public PendingTwoFactorSession? Resolve(string pendingToken, bool consume)
{
var key = CacheKey(pendingToken);
if (!_cache.TryGetValue(key, out PendingTwoFactorSession? session)) return null;
if (consume) _cache.Remove(key);
return session;
}
private static string CacheKey(string token) => $"2fa-pending:{token}";
}