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

RuleWhyHow
UnpredictableAn attacker must not be able to guess a future challenge.≥128 bits from a CSPRNG, hex or base64url.
BoundThe callback comes from a phone; you need to know which browser to sign in.Store the nonce with the browser's session ID.
Short-livedLimits the window for a captured QR code to be used.Expire after ~90 s (shorter for kiosks and doors).
Single-useA 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: reject

Only run this after the signature and URI have been verified.

Order of checks in the callback

  1. Reject missing or non-string address, uri, signature (400).
  2. Parse the URI; host and path must equal your callback, and u=1 must be absent in production (400).
  3. Verify the signature (401).
  4. Atomically consume the nonce (404/409/410 on failure).
  5. 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=1 or plain HTTP callbacks in production.

Try it: the sandbox demo has “Replay” and “Tamper” buttons that show these checks rejecting bad requests.

Back to top