Nonces and single-use URLs
A nonce is a single-use, purpose-bound random token: a password-reset or email-verification code, a one-time download, a confirmation step.
$nonce = Sentinel::nonces()->issue('password-reset', ttl: 900, subject: $user);
$nonce->value; // 43 base64url characters (≈ 256 bits, nonces.length); only its SHA-256 is stored
$nonce->purpose;
$nonce->expiresAt;
Sentinel::nonces()->consume('password-reset', $token, $user); // true exactly once
Sentinel::nonces()->consumeOrFail('password-reset', $token, $user); // NonceRejectedException (403) otherwise- Only SHA-256(value) is stored — a database dump cannot replay a nonce.
- Purpose: ^[a-z0-9][a-z0-9:._-]{0,127}$ (InvalidPurposeException otherwise — on issue and on consume).
- TTL: nonces.ttl (900 s) unless given; a given TTL must be 1–2 592 000 seconds.
- Consuming is one UPDATE statement; success means exactly one row changed. Of eight concurrent consumers on PostgreSQL and MySQL, exactly one wins.
- A wrong purpose, another subject, an expired, consumed or unknown nonce are indistinguishable to the caller (false). A nonce issued without a subject is consumed without one.
Single-use signed URLs
$url = Sentinel::nonces()->signedRoute('exports.download', ['export' => $export->id], ttl: 600);
Route::get('/exports/{export}', DownloadExport::class)
->name('exports.download')
->middleware('sentinel.single-use');signedRoute() issues a nonce for the purpose url:<route name>, adds it as _nonce and returns Laravel’s temporary signed URL. sentinel.single-use first requires a valid Laravel signature — a tampered or expired link is refused before the nonce is touched — then consumes the nonce for that route. The second visit, or a nonce moved to another route, answers 403 (application/problem+json, nonce_rejected).
Flat forms
use RoundlyConsulting\Sentinel\DataTransferObjects\ConsumeNonceRequest;
use RoundlyConsulting\Sentinel\DataTransferObjects\IssueNonceRequest;
use RoundlyConsulting\Sentinel\DataTransferObjects\PruneOptions;
use RoundlyConsulting\Sentinel\DataTransferObjects\SignedRouteRequest;
$nonce = Sentinel::issueNonce(new IssueNonceRequest('password-reset', ttl: 900, subject: $user));
Sentinel::consumeNonce(new ConsumeNonceRequest('password-reset', $nonce->value, $user));
$url = Sentinel::signedRoute(new SignedRouteRequest('exports.download', ['export' => $export->id], ttl: 600));
$pruned = Sentinel::prune(new PruneOptions(idempotency: true, nonces: true, dryRun: true)); // counts onlyStores and pruning
database (the default) uses sentinel_nonces on sentinel.database.connection; cache needs a lock-capable store; bind Contracts\NonceStore for your own. The RFC 9421 verifier remembers each signature’s nonce in the same store — only after the signature verified, so unauthenticated traffic cannot flood it. sentinel:prune (scheduled daily) deletes expired nonces and idempotency keys:
use RoundlyConsulting\Sentinel\DataTransferObjects\PruneOptions;
$result = Sentinel::prune(); // both, PruneResult
$result = Sentinel::prune(new PruneOptions(idempotency: false, nonces: true, dryRun: true));
$result->idempotencyKeys; // deleted (or, on a dry run, would be deleted)
$result->nonces;Cache stores expire entries on their own and report 0. An idempotency key still held by a live lease is never pruned.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.