Guide · 10 minutes · Any language
Nonces & Replay Prevention
A Digi-ID signature proves that a wallet signed a specific URI. The nonce inside that URI is what makes the proof fresh and single-use. Get the nonce right and replay attacks are impossible.
The four rules
| Rule | Why | How |
|---|---|---|
| Unpredictable | An attacker must not be able to guess a future challenge. | ≥128 bits from a CSPRNG, hex or base64url. |
| Bound | The callback comes from a phone; you need to know which browser to sign in. | Store the nonce with the browser's session ID. |
| Short-lived | Limits the window for a captured QR code to be used. | Expire after ~90 s (shorter for kiosks and doors). |
| Single-use | A signed callback replayed later must fail. | Atomically mark the nonce as used on first valid callback. |
Schema
schema.sql
CREATE TABLE digiid_nonces (
nonce CHAR(32) PRIMARY KEY,
session_id VARCHAR(128) NOT NULL,
expires_at TIMESTAMP NOT NULL,
address VARCHAR(40) NULL, -- set once, on the first valid callback
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX digiid_nonces_expires ON digiid_nonces (expires_at);Consume atomically
Two identical callbacks can arrive at the same time. Checking and then updating in two steps lets both succeed. Use a single conditional update and check the affected row count:
consume.sql
UPDATE digiid_nonces
SET address = :address
WHERE nonce = :nonce
AND address IS NULL
AND expires_at > CURRENT_TIMESTAMP;
-- 1 row -> first valid use: sign the session in
-- 0 rows -> unknown, expired or already used: rejectOnly run this after the signature and URI have been verified.
Order of checks in the callback
- Reject missing or non-string
address,uri,signature(400). - Parse the URI; host and path must equal your callback, and
u=1must be absent in production (400). - Verify the signature (401).
- Atomically consume the nonce (404/409/410 on failure).
- Respond 200. The waiting browser picks up the result on its next poll.
Choosing the timeout
- Websites: 90 s is enough for users to unlock their phone and approve. Regenerate the QR code automatically when it expires.
- Doors and kiosks: 20–30 s and a fresh challenge after every scan, so a photographed code is useless.
- Clock skew: compare against the server clock only; never trust timestamps from the client.
Common mistakes
- Reusing one nonce per session instead of per challenge.
- Returning the verified address to anyone who knows the nonce — only the bound session may read it.
- Deleting nonces before the browser has polled, leaving users stuck. Mark used, then clean up later.
- Accepting
u=1or plain HTTP callbacks in production.
Try it: the sandbox demo has “Replay” and “Tamper” buttons that show these checks rejecting bad requests.