namespace Domain.Helpers
{
public class TokenValidationResult
{
/// <summary>Signature is door ons gemaakt (dus niet gemanipuleerd).</summary>
public bool SignatureValid { get; set; }
/// <summary>Het token is nog geldig (niet verlopen, subject klopt, etc.).</summary>
public bool IsValid { get; set; }
public string Subject { get; set; }
public long UnixExpiry { get; set; }
}
public static class ShortTokenHelper
{
public static string GenerateToken(string secret, string subject, TimeSpan validFor)
{
if (string.IsNullOrEmpty(secret))
{
throw new ArgumentException("secret is empty");
}
ArgumentNullException.ThrowIfNull(subject);
long expiry = DateTimeOffset.UtcNow.Add(validFor).ToUnixTimeSeconds();
string data = $"{expiry}:{subject}";
byte[] secretKey = Encoding.UTF8.GetBytes(secret);
using HMACSHA256 hmac = new(secretKey);
string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data))).Replace("+", "-").Replace("/", "_").TrimEnd('=');
// Base64 encode only the subject to handle dots properly
string encodedSubject = Convert.ToBase64String(Encoding.UTF8.GetBytes(subject));
return $"{expiry}.{encodedSubject}.{signature}";
}
public static TokenValidationResult ValidateToken(string secret, string expectedSubject, string tokenInput)
{
TokenValidationResult result = new() { SignatureValid = false, IsValid = false };
ArgumentNullException.ThrowIfNull(tokenInput);
if (string.IsNullOrEmpty(tokenInput))
{
throw new ArgumentException("tokenInput is empty");
}
try
{
// Token format is now: expiry.encodedSubject.signature (no base64 encoding of entire token)
string[] parts = tokenInput.Split('.');
if (parts.Length != 3)
{
return result;
}
if (!long.TryParse(parts[0], out long expiry))
{
return result;
}
// Decode the base64 encoded subject
byte[] subjectBytes = Convert.FromBase64String(parts[1]);
string subjectFromToken = Encoding.UTF8.GetString(subjectBytes);
string signature = parts[2];
// Vul altijd expiry en subject, ook als signature niet klopt
result.Subject = subjectFromToken;
result.UnixExpiry = expiry;
string data = $"{expiry}:{subjectFromToken}";
byte[] secretKey = Encoding.UTF8.GetBytes(secret);
using HMACSHA256 hmac = new(secretKey);
string expectedSig = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data))).Replace("+", "-").Replace("/", "_").TrimEnd('=');
// Zet altijd SignatureValid als de handtekening klopt
result.SignatureValid = signature == expectedSig;
// Alleen geldig als ook signature klopt én checks slagen
if (result.SignatureValid && subjectFromToken == expectedSubject && DateTimeOffset.UtcNow.ToUnixTimeSeconds() <= expiry)
{
result.IsValid = true;
}
return result;
}
catch (FormatException)
{
// Invalid base64 format
return result;
}
}
}
}🧩 ShortTokenHelper – uitleg & gebruik
Deze helper maakt en valideert een kort HMAC-beveiligd token voor tijdelijke authenticatie of verificatie (zoals login-links).
Het tokenformaat is:
<unixExpiry>.<base64(subject)>.<signature>
🔐 Token genereren
string secretKey = "sterk-geheim"; string token = ShortTokenHelper.GenerateToken(secretKey, "user@email.com", TimeSpan.FromDays(3));
→ Output is een compact token dat 3 dagen geldig is.
✅ Token valideren
var result = ShortTokenHelper.ValidateToken(secretKey, "user@email.com", token);
if (result.IsValid)
{
// Token is echt, subject klopt, en nog niet verlopen
}
else if (result.SignatureValid)
{
// Signature klopt, maar subject of expiry niet
}
📘 Structuur van TokenValidationResult
SignatureValid– token niet gemanipuleerdIsValid– token geldig én nog niet verlopenSubject– email of identifier uit tokenUnixExpiry– vervaltijdstip in Unix seconden
🧠 Gebruiksscenario
Wordt o.a. gebruikt voor login-links, “magic links”, of tijdelijke API-verificatie zonder zware JWT.
979000cookie-checkC# ShortTokenHelper