Compare three times.
Record when the file became available, when the link was signed and when the customer tried it. Also check the lifetime of the credentials used to sign it. The URL’s apparent expiry is not always the only limit.
Use the timestamp converter and time difference calculator to compare observations in UTC. Do not paste a complete production signing URL into a public support thread.
Issue a fresh link after an access check.
Store the report ID and object identity in your app. When an authorised customer asks to download it, check access again and create a short-lived URL. A link embedded in a long-lived email or stale page can expire while the file remains valid.
async function downloadReport(user, reportId) {
const report = await loadReport(reportId);
await requireReportAccess(user, report);
const signedUrl = await signDownload(report.objectKey);
return { url: signedUrl };
}This is a control-flow sketch. Implement the authorisation check and signing call on your server. Never accept an arbitrary customer-supplied storage path for signing.
Distinguish expiry from other denials.
A 403 is not proof of an expired link. Inspect the storage provider’s error code, object existence, signing principal and policy. Clock differences, revoked permissions or a changed object key may produce a different cause.
For S3, temporary credentials can expire earlier than a requested URL duration. Provider behaviour differs, so use your storage provider’s documentation. Do not solve a private-download failure by opening the bucket to the public.
Test a delayed download.
Generate a development link, wait past its deliberately short expiry and confirm the old link is denied. Ask your app for a fresh link as the correct user; then repeat as the wrong user. Check the resulting file as well as the HTTP status.
Reference: Amazon S3 presigned URL expiry and permissions.