Signed URLs for Private Files
Short-lived links to private files that work without a session — for native app clients and embedded HTML (inline email images) that cannot send cookies or custom headers.
The contract: minting is the authorization
File::mintSignedUrl() is the authorization statement. Only code that has
already verified the viewer may access the file calls it — e.g. a mailbox
thread fetch that has verified viewer scope mints links for the thread's
attachments as it builds the response. There is no hook registry and no
plugin callback in the serving path; authorization stays in the feature code
that knows the rules.
Minting
$url = $file->mintSignedUrl($size_key, $ttl_seconds, $format);
// e.g. /uploads/thumb/photo.jpg?expires=1751500000&sig=ab12...$size_key—'original'(default) or an ImageSizeRegistry variant key.$ttl_seconds— keep short; default 300 (5 minutes). Consumers that re-fetch their parent resource for fresh links use the default; a consumer that mints once per page open and sits idle (the web mail readers' inline images) uses a longer TTL — 3600 — and accepts that a link outliving it renders broken until the page is reopened.$format—'short'(relative, default) or'full'(absolute).
/uploads pattern — it routes through
serve.php's gate, never a bucket URL.Validation and serving
The /uploads/* route in serve.php checks expires/sig query
parameters before the ownership gate:
- Valid and unexpired (constant-time compare) → the file is served with no session consulted.
- Invalid, expired, or absent → falls through to the normal
is_viewable()gate. A signed miss is never its own error state: an expired link in a logged-in owner's browser still works via their session; a stranger gets the same 404 they would get with no link at all.
Cache-Control: private, no-store — a cached
copy must not outlive the grant. Private cloud-stored files keep their
never-302 rule: a valid signature streams the bytes through PHP from the
private bucket, exactly like the sessioned path.The signing key
A dedicated 32-byte key, generated on first mint and stored
SecretBox-encrypted in stg_settings under file_signed_url_key. It is
deliberately separate from secret_box_key (key separation): deleting the
row rotates the key, which invalidates every outstanding signed URL and
nothing else — a non-event given short TTLs.
Composition: Drive share links
A Drive public share link (/s/{token}, see Drive) is the *durable,
revocable grant; the signed URL stays the short-lived transport. The share
page authorizes the visitor against the link (live, not revoked, password
satisfied), then mints a fresh signed URL per download. Revoking or expiring the
link stops new signed URLs from being issued; any already-minted URL simply lapses
at its short TTL. The two layers are independent — the link controls whether a
visitor may fetch, the signed URL controls this one fetch*.
Tests
tests/functional/files/signed_urls_test.php (see Testing) — covers no-session
serving, expiry, tamper, size-key binding, and the ownership-gate fallback.