Hello there!๐๐งโโ๏ธ Ever wondered how systems prevent an attacker from reusing an old request, or how that six-digit code from your authenticator app works? Two concepts sit at the heart of this: cryptographic nonces (numbers used once) and one-time passwords (OTP). Both are about ensuring something is used only onceโnonces protect integrity and replay, OTPs prove "you have this secret right now." As a senior engineer, using them correctly is key for secure auth and APIs.
Think of a nonce like a concert ticket with a unique barcode: you can't reuse it, and the venue can tell if someone tries. An OTP is like a temporary PIN that expires in 30 secondsโvalid once, for a short time. Let's see how they work and where to use them.
Overview
In this guide we'll cover:
- Cryptographic nonce โ What it is, why uniqueness matters, and common use cases
- Nonce in practice โ Replay protection, CSRF tokens, and encryption (IVs)
- One-Time Passwords (OTP) โ HOTP vs TOTP, SMS vs app-based, and when to use each
- Implementing OTP โ TOTP in C# and best practices for verification
We'll use C# and .NET for code examples, with the same ideas applying in other stacks.
1. Cryptographic Nonce
What Is a Nonce?
A nonce is a value that is used only once in a given context. The word comes from "number once." In cryptography and security we usually mean:
- Unpredictable โ An attacker cannot guess the next nonce (so it's typically random or counter-based with a secret).
- Unique โ The same value is not reused in the same scope (e.g. same key, same session, or same time window).
Real-world analogy: A single-use voucher code. Each code is valid once; after redemption, it's rejected. You can't copy an old receipt and reuse it.
Why Nonces Matter
Nonces help with:
- Replay protection โ Block an attacker from re-sending an old valid message (e.g. a payment or login request).
- Uniqueness in encryption โ Many modes (e.g. CBC, GCM) require a unique initialization vector (IV) per encryption so the same plaintext doesn't produce the same ciphertext.
- CSRF mitigation โ A token that the server issues and checks so the request came from the real UI, not a forged form.
- Challengeโresponse โ Prove freshness (e.g. the client just computed something from a fresh challenge).
Without a nonce (or similar), an attacker could replay a captured request and the server might accept it again.
2. Nonce in Practice
2.1 Replay Protection for APIs
You issue a nonce per request (or per operation). The client sends it; the server remembers "already used" nonces and rejects duplicates.
public class ReplayProtectionService
{
private readonly ICache _cache; // e.g. distributed cache with expiry
private const int NonceValidityWindowSeconds = 300; // 5 minutes
public string GenerateNonce()
{
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes);
}
public bool TryConsumeNonce(string nonce)
{
if (string.IsNullOrEmpty(nonce)) return false;
var key = $"nonce:{nonce}";
// Only add if not already present (idempotent "use once")
if (_cache.TryAdd(key, true, TimeSpan.FromSeconds(NonceValidityWindowSeconds)))
return true;
// Already used or expired
return false;
}
}
Flow: Client gets a nonce (e.g. from a "get nonce" endpoint or first request), includes it in the request. Server calls TryConsumeNonce(nonce). If it returns true, the nonce was fresh and is now marked used; if false, reject the request (replay or invalid).
2.2 CSRF Tokens (Nonce-Like)
In web apps, a CSRF token is like a nonce: the server generates a random value, puts it in the form (or a header), and checks it on submit. Same token twice or missing token โ reject.
// Generating a CSRF token (e.g. in a filter or page)
public string GenerateCsrfToken()
{
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes);
}
// Store in session or signed cookie; validate on POST/PUT/DELETE
// that the incoming token matches the one issued for this session
The important part: validate on the server and bind the token to the session (or user). Don't rely on the client to "remember" the nonce for youโthe server must be the source of truth.
2.3 Initialization Vector (IV) in Encryption
In symmetric encryption (e.g. AES), many modes need an IV that is unique per encryption under the same key. Reusing an IV with the same key can leak information or break security.
public byte[] Encrypt(byte[] plaintext, byte[] key)
{
using var aes = Aes.Create();
aes.Key = key;
aes.GenerateIV(); // New random IV per call โ treat as nonce
using var encryptor = aes.CreateEncryptor();
var ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
// Prepend IV to ciphertext (IV is not secret, but must be unique)
var result = new byte[aes.IV.Length + ciphertext.Length];
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
Buffer.BlockCopy(ciphertext, 0, result, aes.IV.Length, ciphertext.Length);
return result;
}
Rule: Never reuse an IV with the same key. Generate a new one (e.g. random) for each encryption, or use a counter/nonce scheme that your mode specifies (e.g. GCM).
3. One-Time Passwords (OTP)
What Is an OTP?
An OTP is a password or code that is valid for one use (and often for a short time). It proves that the user has access to something only they have (a device, an app, or a channel) at that moment.
Real-world analogy: A code sent to your phone to confirm a login. Once you type it in, that code is consumed. The next login needs a new code.
HOTP vs TOTP
| HOTP | TOTP | |
|---|---|---|
| Stands for | HMAC-based One-Time Password | Time-based One-Time Password |
| Standard | RFC 4226 | RFC 6238 (extends HOTP) |
| Input | Counter (incremented each use) | Time step (e.g. 30-second window) |
| Typical use | Hardware tokens, some legacy systems | Authenticator apps (Google, Microsoft, Authy), 2FA |
- HOTP โ Code derived from a shared secret + counter. Server and client both know the secret; counter increments so each code is different. No time dependency.
- TOTP โ Same idea, but the "counter" is current time / step (e.g. floor(current time / 30 seconds)). So the code changes every 30 seconds. Most 2FA apps use TOTP.
SMS OTP vs App-Based (TOTP)
- SMS OTP โ Server generates a short-lived code (e.g. 6 digits), sends it via SMS. User types it in. Problems: SMS is not encrypted, numbers can be ported, delivery delays, cost. Use for low-risk or legacy flows; prefer app-based when possible.
- App-based TOTP โ User and server share a secret (shown as QR or key). The app (e.g. Google Authenticator) computes the same 6-digit code from the secret + current time. No network needed for the code itself; server does the same math and compares.
For new systems, TOTP (authenticator app) is usually the better default for 2FA.
4. Implementing OTP (TOTP in C#)
4.1 Generating a TOTP Secret and QR Code
The server generates a random secret per user and stores it. The user enrolls by scanning a QR code (or entering the key) into an authenticator app.
using System.Security.Cryptography;
public class TotpService
{
private const int StepSeconds = 30;
private const int DigitCount = 6;
public string GenerateSecret()
{
var bytes = new byte[20]; // 160 bits typical for TOTP
RandomNumberGenerator.Fill(bytes);
return Base32Encode(bytes); // TOTP uses Base32 (e.g. RFC 4648)
}
public int ComputeTotp(string secretBase32)
{
var key = Base32Decode(secretBase32);
var counter = (ulong)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() / StepSeconds);
return ComputeHotp(key, counter);
}
public bool ValidateTotp(string secretBase32, int code, int windowTolerance = 1)
{
var key = Base32Decode(secretBase32);
var counter = (ulong)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() / StepSeconds);
for (var offset = -windowTolerance; offset <= windowTolerance; offset++)
{
if (ComputeHotp(key, counter + (ulong)offset) == code)
return true;
}
return false;
}
private static int ComputeHotp(byte[] key, ulong counter)
{
var counterBytes = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian)
Array.Reverse(counterBytes);
var hash = HMACSHA1.HashData(key, counterBytes);
var offset = hash[^1] & 0x0F;
var binary = ((hash[offset] & 0x7F) << 24)
| (hash[offset + 1] << 16)
| (hash[offset + 2] << 8)
| hash[offset + 3];
var digits = (int)Math.Pow(10, DigitCount);
return binary % digits;
}
// TOTP secrets use Base32 (RFC 4648). Omitted here; use OtpNet or a Base32 library.
private static string Base32Encode(byte[] data) { /* ... */ return ""; }
private static byte[] Base32Decode(string input) { /* ... */ return Array.Empty<byte>(); }
}
Note: TOTP expects the secret in Base32. The snippet above is conceptual; in production use a well-tested library (e.g. OtpNet) for Base32 and HOTP/TOTP to avoid encoding or off-by-one bugs.
4.2 Using a Library (OtpNet)
// NuGet: OtpNet
using OtpNet;
var secret = KeyGeneration.GenerateRandomKey(20);
var totp = new Totp(secret, step: 30, totpSize: 6);
string code = totp.ComputeTotp(DateTimeOffset.UtcNow);
bool valid = totp.VerifyTotp(DateTimeOffset.UtcNow, code, out _);
Verification: Allow a small time window (e.g. ยฑ1 step = ยฑ30 seconds) to account for clock skew; reject codes that are too old. And consume the code for sensitive operations (e.g. disable reuse of the same code for that user within a short period) so it's truly one-time.
4.3 SMS OTP (High Level)
For SMS, the server generates a short random code (e.g. 6 digits), stores it in cache with a short TTL (e.g. 5โ10 minutes) and the user id, sends it via an SMS provider (Twilio, etc.), and when the user submits the code, you compare and then remove it from cache so it can't be used again.
public class SmsOtpService
{
private readonly ICache _cache;
private readonly ISmsSender _smsSender;
private const int Length = 6;
private static readonly TimeSpan Validity = TimeSpan.FromMinutes(5);
public async Task SendOtpAsync(string userId, string phoneNumber, CancellationToken ct = default)
{
var code = RandomNumberGenerator.GetInt32(0, (int)Math.Pow(10, Length)).ToString("D6");
await _cache.SetAsync($"otp:{userId}", code, Validity, ct);
await _smsSender.SendAsync(phoneNumber, $"Your code is {code}. Valid for {Validity.TotalMinutes} min.", ct);
}
public async Task<bool> VerifyOtpAsync(string userId, string code, CancellationToken ct = default)
{
var key = $"otp:{userId}";
var stored = await _cache.GetAsync<string>(key, ct);
if (stored is null || stored != code) return false;
await _cache.RemoveAsync(key, ct); // One-time: consume after use
return true;
}
}
Best Practices Summary
Nonces:
- Use a cryptographically secure random source (
RandomNumberGenerator.Fill), notRandom. - Bind nonce validity to time (short window) and one-time use (store "used" and reject duplicates).
- For IVs: never reuse an IV with the same key; generate a new one per encryption.
OTP:
- Prefer TOTP (authenticator app) over SMS when possible.
- Time window: allow ยฑ1 step for TOTP to handle clock skew; don't allow very old codes.
- One-time: after successful verification, invalidate the code so it can't be reused.
- Store TOTP secrets securely (e.g. encrypted at rest); treat them like passwords.
Conclusion
Cryptographic nonces give you uniqueness and replay protection for requests, CSRF tokens, and encryption (IVs). One-time passwords prove possession of a secret at a given time: HOTP is counter-based, TOTP is time-based and is what most authenticator apps use. Use nonces to make operations non-replayable and IVs unique; use OTPs to strengthen authentication (2FA) and one-time verification (e.g. password reset, sensitive actions).
Key takeaways:
- Nonce โ Unpredictable, used-once value; use for replay protection, CSRF, and unique IVs per encryption.
- Replay protection โ Issue and consume nonces server-side with a validity window; reject duplicates.
- OTP โ One-time password; TOTP (time-based) is standard for 2FA; prefer app-based over SMS when possible.
- TOTP โ Shared secret + time step; verify with a small window and consume the code after use.
Use nonces and OTPs where freshness and one-time use matterโyour APIs and users will be safer for it.
Stay secure, and happy coding! ๐๐
Top comments (0)