Generating the nonce
The nonce is a one-time random string that your server creates for every page load. It is what stops a token from being used by anyone except the page that asked for it. A good one looks like this: 8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0.
| Rule | Detail |
|---|---|
| Who creates it | Your server, inside POST /api/nonce. Never the browser and never SuperPreneur |
| Randomness | From a cryptographically secure source. 24 random bytes is the recommended size |
| Format | URL-safe Base64 without padding: letters, digits, - and _. Length 16 to 128; 24 bytes gives 32 characters. Pattern ^[A-Za-z0-9_-]{16,128}$. SuperPreneur refuses anything else |
| Lifetime | 2 minutes |
| Use | Once. Remove it the moment /api/session receives it, before you call the identity provider |
| Where it lives | On your server, looked up by the nonce itself. Never in a cookie or the URL |
| Never | Build it from a user id, the time or a counter; reuse it across page loads; use Math.random(), rand() or a version 1 UUID |
Creating it#
# Python 3
import secrets
nonce = secrets.token_urlsafe(24) # 32 characters
// Node.js 16 or newer
const nonce = require('node:crypto').randomBytes(24).toString('base64url');
// Go
b := make([]byte, 24)
if _, err := rand.Read(b); err != nil { /* handle it */ } // crypto/rand
nonce := base64.RawURLEncoding.EncodeToString(b) // encoding/base64
// Java 8 or newer
byte[] b = new byte[24];
new SecureRandom().nextBytes(b);
String nonce = Base64.getUrlEncoder().withoutPadding().encodeToString(b);
// PHP 7 or newer
$nonce = rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
The Python, Node, Go and Java versions were run and each produced a valid 32-character nonce. The PHP line was not run; it is the standard recipe for URL-safe Base64.
Storing it and using it up#
On one server a dictionary in memory is enough: store nonce -> expiry time, delete expired entries when you add new ones, and delete the nonce as soon as it is used. The complete examples do exactly this.
If you run several server instances, they must share the nonces (and your sessions), or a request can land on an instance that has never seen the nonce. With Redis:
SET nonce:<nonce> 1 EX 120 # when you create it: expires in 120 seconds
GETDEL nonce:<nonce> # when /api/session arrives: returns 1 once, then nothing
GETDEL reads and deletes in one step, so two requests with the same nonce can never both succeed. It needs Redis 6.2 or newer; on older versions use DEL and check that it returned 1.
Why it matters#
Without a nonce, anyone who got hold of a token (from a screenshot, a log, a shared link) could present it to your server within its 60 seconds. With one, the token only works for the page load whose nonce your server issued, and only once. Two checks protect you: your server refuses a nonce it did not issue or has already used, and the identity provider refuses a token whose nonce differs from the one you send.