A one-time URL is a temporary bearer credential, not just a random query string. To make it genuinely single-use, generate an unpredictable token, store only a protected digest, bind it to one purpose and an expiry, then atomically perform the action and mark the token consumed. The implementation below updates the historical PHP Master pattern with modern PHP randomness, transaction safety, scanner-resistant request handling and leakage controls.
What a one-time URL actually does
A link such as https://example.com/verify-email?token=... grants possession-based authority for one narrowly defined operation. Typical uses include email verification, password resets, invitations, email-address changes, destructive-action confirmation, workflow approval, unsubscribe requests and temporary downloads.
The token should authorize only that operation. It is not a replacement for a login session and it does not prove who clicked the link.
The three properties that make it one-time
- Unpredictability: generate the token with a cryptographically secure random-number generator.
- Expiration: reject it after an absolute UTC deadline.
- Atomic consumption: perform the business action and invalidate the token in one transaction, or use an equivalent atomic state transition.
A signed URL can detect tampering and enforce an expiry, but it remains reusable unless the server records whether it has been consumed.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
- Standard OATH compliant TOTP token (time based)
- 6-digit OTP code with countdown time bar
- Zero footprint: no need for the end user to install any software
- Secure, sturdy, and long-life hardware design
- Easy to use - Portable key chain design. These tokens will only work with Symantec VIP Access. These tokens will not work for any other Multi-Factor Authentication services, besides Symantec VIP Access.
Why sha1(uniqid(...)) is legacy code
The original PHP Master example, published in 2013 and updated by SitePoint in 2024, creates a value with sha1(uniqid($username, true)), stores a 40-character SHA-1 result and deletes it after use. That is useful historical context, but it is not appropriate new security code: uniqid() is time-based rather than a cryptographic random source, and hashing a predictable input does not make it unpredictable. See the original example at SitePoint.
Use random_bytes(), which PHP documents as suitable for secrets and encryption keys (PHP manual):
<?php
$rawToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $rawToken);
$expiresAt = (new DateTimeImmutable('now', new DateTimeZone('UTC')))
->modify('+30 minutes');
This creates 32 random bytes (256 bits of token material), represented as a 64-character hexadecimal value. The raw value is delivered once; the database stores the 64-character SHA-256 digest. For additional defense in depth, use hash_hmac('sha256', $rawToken, $_ENV['TOKEN_PEPPER']) with a server-held secret.
Database design
A practical MySQL schema binds each token to a purpose and keeps enough lifecycle information for investigation and support:
Rank #2
- OTP token that provides secure remote access with strong authentication
- Easy to use and easy to carry
- Expected battery life is approximately 7 years
CREATE TABLE one_time_tokens (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
token_hash CHAR(64) NOT NULL,
user_id BIGINT UNSIGNED NULL,
purpose VARCHAR(50) NOT NULL,
expires_at DATETIME NOT NULL,
used_at DATETIME NULL,
created_at DATETIME NOT NULL,
used_ip VARBINARY(16) NULL,
used_user_agent VARCHAR(500) NULL,
UNIQUE KEY uq_one_time_token_hash (token_hash),
KEY ix_token_lookup (purpose, token_hash, expires_at)
);
The minimum is token_hash, purpose, expires_at and either used_at or deletion status. A purpose check prevents an email-verification token being accepted by a password-reset endpoint. IP and user-agent fields should be retained only under an appropriate privacy policy.
Generate, store and send the link
- Generate the raw token and digest it.
- Insert the digest, subject identifier, purpose and UTC expiry.
- Construct the URL from a configured HTTPS origin.
- Send the raw token through your email or notification system; do not put it in ordinary application logs.
$rawToken = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $rawToken);
$expiresAt = (new DateTimeImmutable('now', new DateTimeZone('UTC')))
->modify('+30 minutes');
$stmt = $pdo->prepare(
'INSERT INTO one_time_tokens
(token_hash, user_id, purpose, expires_at, created_at)
VALUES (:token_hash, :user_id, :purpose, :expires_at, UTC_TIMESTAMP())'
);
$stmt->execute([
':token_hash' => $tokenHash,
':user_id' => $userId,
':purpose' => 'email-verification',
':expires_at' => $expiresAt->format('Y-m-d H:i:s'),
]);
$url = 'https://example.com/verify-email?token=' . rawurlencode($rawToken);
Never derive the origin from an untrusted Host header. Configure a canonical application URL and trusted hosts, particularly for password-reset mail. Do not add an email address, internal ID or other personal data when the token alone identifies the server-side record.
Consume the token safely
The following PDO example validates format, locks the row, performs email activation and records consumption in the same transaction:
<?php
$rawToken = $_GET['token'] ?? '';
if (!is_string($rawToken) || !preg_match('/^[a-f0-9]{64}$/i', $rawToken)) {
http_response_code(400);
exit('This link is invalid or has expired.');
}
$tokenHash = hash('sha256', strtolower($rawToken));
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare(
'SELECT id, user_id
FROM one_time_tokens
WHERE token_hash = :token_hash
AND purpose = :purpose
AND used_at IS NULL
AND expires_at > UTC_TIMESTAMP()
FOR UPDATE'
);
$stmt->execute([
':token_hash' => $tokenHash,
':purpose' => 'email-verification',
]);
$token = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$token) {
$pdo->rollBack();
http_response_code(400);
exit('This link is invalid or has expired.');
}
$activate = $pdo->prepare(
'UPDATE users
SET email_verified_at = UTC_TIMESTAMP()
WHERE id = :user_id AND email_verified_at IS NULL'
);
$activate->execute([':user_id' => $token['user_id']]);
$consume = $pdo->prepare(
'UPDATE one_time_tokens
SET used_at = UTC_TIMESTAMP()
WHERE id = :id AND used_at IS NULL'
);
$consume->execute([':id' => $token['id']]);
if ($consume->rowCount() !== 1) {
throw new RuntimeException('Token was already consumed.');
}
$pdo->commit();
echo 'Your email address has been verified.';
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log($e->getMessage());
http_response_code(500);
echo 'The request could not be completed.';
}
Without the transaction and row lock, two concurrent requests can both read an unused token before either one deletes or updates it. An atomic alternative is a conditional UPDATE requiring used_at IS NULL and an affected-row count of one; use that only when you have defined what happens if the subsequent business action fails.
Rank #3
- Works with authentication systems that support TOTP tokens: Google, Facebook, Coinbase, GDAX, Dropbox, GitHub, Kickstarter, Microsoft, TeamViewer, etc.
- Programmable an unlimited number of times. Features syncable clock to prevent issues with drift
- About half the size of a credit card and just as thick-easily keep multiple cards in wallet
- Works with "Token2 Token Burner" or "Protectimus TOTP Burner", both available in the Google Play Store. Now also iOS compatible (iPhone 7 and later)
- More secure than software token as your codes cannot be intercepted by malware on your phone.
Delete rows or retain used_at?
| Choice | Advantages | Trade-off |
|---|---|---|
| Delete on success | Simple lookup and a small active table | No audit history for support or incident investigation |
Set used_at |
Preserves lifecycle and abuse evidence; distinguishes reuse internally | Requires cleanup and an used_at IS NULL condition on every lookup |
Use used_at for password resets, approvals and other sensitive workflows. Deletion can be adequate for disposable, low-audit records.
Expiry, revocation and cleanup
Store an absolute UTC deadline and reject when expires_at <= UTC_TIMESTAMP(). A 24-hour window in the historical example is not a PHP default or a security guarantee. Password resets commonly use 15–60 minutes; destructive confirmations may use only a few minutes; invitations can require hours or days. Choose the shortest period compatible with delivery delays and the action’s sensitivity.
Decide what resending does: revoke every earlier token, revoke only the previous active token, or allow several links. For password resets, invalidating earlier tokens is usually easier to reason about. A scheduled cleanup can remove expired and old consumed records:
DELETE FROM one_time_tokens
WHERE expires_at < UTC_TIMESTAMP()
OR used_at < UTC_TIMESTAMP() - INTERVAL 30 DAY;
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Do not consume on an unintentional GET
Mail security scanners, antivirus gateways and browser prefetchers may request a link automatically. If a GET immediately changes state, a scanner can consume the token before the user sees the page.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesRank #4
- OTP Token in card format that provides secure remote access with strong authentication
- Easy to use and easy to carry, same size as a credit card
- Zero footprint; No software on end-user PCs
- Compliant to OATH open standard (time based - 6 digits)
- Expected battery life is 3 years or approximately 15,000 clicks
- GET: validate the token and display the intended action without consuming it.
- POST: require a deliberate submission, CSRF protection and the transactional consume-and-act operation.
This is especially important for password resets: GET should show the reset form, while changing the password happens only on POST. A one-click experience is simpler but cannot reliably distinguish a person from an automated fetcher.
Bearer-token leakage and operational defenses
- Serve links only over HTTPS.
- Use
Referrer-Policy: no-referrerand avoid third-party assets on token pages. - Redact query strings from web-server, proxy, analytics and exception logs.
- After validating a token, redirect to a clean URL or replace the browser history entry.
- Keep lifetimes short, rate-limit attempts and notify the account owner after sensitive actions.
- Require an authenticated session for especially sensitive operations when practical.
Anyone who obtains the URL can generally use it before consumption. A one-time mechanism limits replay after successful use; it does not prevent forwarding, screenshots, browser-history exposure, email compromise or a stolen log entry.
Password-reset-specific requirements
- Never email an existing password.
- Return the same outward-facing response whether an account exists, with rate limiting to reduce enumeration.
- Keep reset tokens short-lived and single-use; consider revoking earlier tokens when issuing a new one.
- After a successful reset, notify the user and offer session invalidation or revoke relevant sessions.
Laravel provides database- or cache-backed reset-token services and the surrounding workflow; use them instead of reimplementing password recovery when your application is already Laravel-based (Laravel password resets).
Signed URLs and S3 presigned URLs are different
Laravel’s temporary signed routes put an expiry and signature on a URL and reject tampering or late requests (Laravel signed URLs). They do not automatically remember that a request succeeded. Add a nonce and server-side consumption state when single use is required.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Amazon S3 presigned URLs are time-limited bearer access to an object, not inherently one-download links. S3 evaluates expiry when a request is made; an in-progress download can continue after expiry, while a later retry may fail. Temporary credentials can shorten the effective lifetime (AWS documentation). For one-download behavior, validate and consume an application token first, then issue the S3 URL, while explicitly deciding how retries and partial downloads work.
Quick Recap
Testing checklist
- A valid unused token succeeds exactly once.
- A second request fails.
- Expired, malformed, revoked and wrong-purpose tokens fail.
- Two simultaneous requests produce only one successful action.
- A failed business action leaves token state consistent.
- A scanner-like GET does not consume a token in the two-step design.
- Cleanup removes expired and aged consumed records.
- Logs and referrers do not expose complete token values.
Implementation checklist
- Use
random_bytes(), never timestamps, IDs oruniqid()for bearer secrets. - Store a SHA-256 digest (or keyed HMAC digest), not the raw token.
- Bind every record to a purpose, subject and UTC expiry.
- Use a transaction with row locking or an atomic state transition.
- Prefer POST plus CSRF protection for state-changing actions.
- Keep the origin trusted, links HTTPS-only and responses generic where enumeration matters.
- Use
hash_equals()when comparing secret strings in application code (PHP manual).
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

