Implementing Unique Download Link Generation
A unique link is the primary access control mechanism for digital products. Unlike direct file links, a unique token prevents guessing another purchase's URL and binds the download to a specific transaction. Proper implementation requires a cryptographically strong generator, brute-force protection, and a mechanism for rotating compromised links.
Token Requirements
- Unpredictability — cannot be guessed or derived from order number sequence
- Sufficient entropy — minimum 128 bits (32 bytes random → 64 hex chars)
- Uniqueness — UNIQUE constraint in table with collision handling
- Short URL — token shouldn't exceed 64–80 characters
Token Generation
class DownloadTokenGenerator
{
/**
* Generate 32 random bytes via CSPRNG and convert to hex
* random_bytes() uses /dev/urandom on Linux, CryptGenRandom on Windows
*/
public function generate(): string
{
return bin2hex(random_bytes(32)); // 64 characters
}
/**
* Generate with uniqueness guarantee
*/
public function generateUnique(int $maxAttempts = 5): string
{
for ($i = 0; $i < $maxAttempts; $i++) {
$token = $this->generate();
if (!DigitalOrderDownload::where('token', $token)->exists()) {
return $token;
}
}
// 5 collisions in a row with 64-char hex is statistically impossible,
// but treat as hard error
throw new TokenGenerationFailedException('Failed to generate unique token');
}
}
Short Tokens (Optional)
For readable links (SMS, QR codes), use Base62:
class ShortTokenGenerator
{
private const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
/**
* 16 Base62 characters = ~95 bits entropy — sufficient for download links
*/
public function generate(int $length = 16): string
{
$token = '';
$bytes = random_bytes($length);
for ($i = 0; $i < $length; $i++) {
$token .= self::ALPHABET[ord($bytes[$i]) % 62];
}
return $token;
}
}
URL Structure
https://example.com/dl/a3f8c92b1d4e7f0a2b5c8d9e1f3g4h5j
Short path /dl/ — not /download/ — to avoid revealing system logic. Token in path, not query string — query string is logged in access_log and may leak to referrers.
// routes/web.php
Route::get('/dl/{token}', [DigitalDownloadController::class, 'show'])
->name('digital.download.show')
->middleware(['throttle:30,1']); // max 30 requests per minute
Route::get('/dl/{token}/get', [DigitalDownloadController::class, 'download'])
->name('digital.download.get')
->middleware(['throttle:10,1']); // actual download — stricter
Rate Limiting Against Brute Force
// app/Http/Middleware/ThrottleDownloadTokens.php
class ThrottleDownloadAttempts
{
public function handle(Request $request, Closure $next): Response
{
$key = 'download-attempt:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 20)) {
// Too many attempts from this IP
abort(429, 'Too many requests. Try again later.');
}
RateLimiter::hit($key, 60); // 1 minute window
$response = $next($request);
// Invalid token (404) also counts as attempt
if ($response->getStatusCode() === 404) {
RateLimiter::hit('invalid-token:' . $request->ip(), 3600);
if (RateLimiter::tooManyAttempts('invalid-token:' . $request->ip(), 10)) {
// More than 10 nonexistent tokens from one IP per hour — block
app(IpBlocklistService::class)->block($request->ip(), reason: 'token_bruteforce');
}
}
return $response;
}
}
Rotating Compromised Links
If a customer reports link was published or stolen:
class RotateDownloadTokenAction
{
public function execute(DigitalOrderDownload $download, string $reason): DigitalOrderDownload
{
// Invalidate old token
$download->update(['is_revoked' => true]);
// Log reason
DownloadTokenRotationLog::create([
'original_token' => $download->token,
'reason' => $reason,
'rotated_at' => now(),
'rotated_by' => auth()->id(),
]);
// Create new token with same parameters
$newDownload = DigitalOrderDownload::create([
'order_item_id' => $download->order_item_id,
'digital_product_id' => $download->digital_product_id,
'token' => app(DownloadTokenGenerator::class)->generateUnique(),
'downloads_count' => 0, // counter reset
'downloads_limit' => $download->downloads_limit,
'expires_at' => $download->expires_at,
]);
// Send new link
Mail::to($download->orderItem->order->email)
->send(new DownloadTokenRotatedMail($newDownload));
return $newDownload;
}
}
Signed URLs (Alternative Approach)
Instead of storing token in DB, use HMAC-signed URLs. Advantage — no DB record per purchase needed. Disadvantage — can't invalidate individual token without key.
// Generate signed URL with expiration
$url = URL::temporarySignedRoute(
'digital.download.get',
now()->addDays(30),
['order_item_id' => $item->id]
);
// Validation in controller — automatic via SignedMiddleware
Route::get('/dl/signed/{order_item_id}', [DigitalDownloadController::class, 'signedDownload'])
->name('digital.download.get')
->middleware('signed');
Signed URLs are used when granting access to many users immediately (e.g., corporate license for 100 employees) without creating 100 DB records.
Anomaly Monitoring
// Alert: one token downloaded from many IPs
$suspiciousDownloads = DownloadEvent::selectRaw('
digital_order_download_id,
COUNT(DISTINCT ip_address) as unique_ips,
COUNT(*) as total_downloads
')
->where('downloaded_at', '>', now()->subHours(24))
->groupBy('digital_order_download_id')
->having('unique_ips', '>', 5)
->get();
Timelines
Basic token generation + brute-force protection — 1–2 working days. Token rotation, anomaly monitoring, signed URLs as alternative mechanism — another 2 days.







