C# ShortTokenHelper

Date: 2025-10-08
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 gemanipuleerd
  • IsValid – token geldig én nog niet verlopen
  • Subject – email of identifier uit token
  • UnixExpiry – vervaltijdstip in Unix seconden

🧠 Gebruiksscenario

Wordt o.a. gebruikt voor login-links, “magic links”, of tijdelijke API-verificatie zonder zware JWT.

97900cookie-checkC# ShortTokenHelper