using System.Security.Cryptography; using Microsoft.Extensions.Caching.Memory; namespace JobTrackerApi.Services; public sealed record PendingTwoFactorSession(string UserId, bool RememberMe); public interface ITwoFactorPendingTokenService { string IssuePendingToken(string userId, bool rememberMe); 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) { var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant(); _cache.Set(CacheKey(token), new PendingTwoFactorSession(userId, rememberMe), 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}"; }