Designing share links you can revoke (capability URLs done right)
A share link is a password in a URL. If it cannot be revoked, you have built a permanent key. Here is a pattern that keeps the convenience and gives you an off switch.
Two kinds of link
| Self-contained signed URL | Token looked up on every request | |
|---|---|---|
| Examples | S3/GCS presigned URLs, HMAC-signed query strings, JWT in a link | /s/Xk3… mapped to a row in your database |
| Revoke one link | No, only by expiry, rotating keys or deleting the object | Yes, instantly |
| Change who may open it later | No, the rules are baked in | Yes, rules are checked at request time |
| Cost | No lookup | One indexed lookup per request |
Use self-contained signatures for the last hop only (a 60–300 second redirect to storage), never as the link people keep. See why presigned URLs cannot be revoked.
The pattern
- Generate at least 128 bits from a cryptographic random source (for example
secrets.token_urlsafe(16)in Python orcrypto.getRandomValuesin the browser). Never derive it from an ID, a timestamp or a counter. The UUID and password generator shows what that randomness looks like. - Store a hash of the token (SHA-256 is fine for high-entropy tokens; try the hash generator), with the file ID, audience rule (named people, signed-in members, anyone with the link), expiry, and a
revoked_atcolumn. A database leak then does not leak working links. - Check on every request, including downloads, thumbnails and previews, not just the page that lists the file. Expired or revoked returns
404or410 Gone; a signed-in user who is not allowed gets403; a link that needs sign-in gets401or a sign-in redirect (see 401 vs 403). - Serve privately:
Cache-Control: private, no-storeon anything behind the link so shared caches and the browser do not keep it (no-cache vs no-store). If you redirect to storage, make that URL expire in minutes. - Do not leak the token:
Referrer-Policy: no-referrerso outbound links and third-party resources on the page do not receive it; keep tokens out of logs and analytics (log a short prefix of the hash if you must). - Revoke and replace: “Reset link” sets
revoked_atand issues a new token; the old one fails on its next use. - Rate-limit token lookups per IP so nobody can guess links, and return the same response for “never existed” and “revoked” if you do not want to confirm a link was once real.
Be honest in the interface
Show the exact content and the audience before someone creates a link (“Anyone with this link can view this photo”). When they revoke, say what it does and does not do: future visits are refused, but copies already downloaded or screenshotted stay with whoever has them. People make better decisions when the screen tells the truth.
Sessions and tokens count too
If the viewer is signed in, their session is part of the check. A long-lived stateless token means removed users keep access until it expires; short-lived access tokens with revocable refresh tokens fix that. See how to revoke a JWT.
For a one-time secret rather than a file, a link that destroys itself after the first read is simpler still; that is how Fusenote works, with the key kept in the part of the link after # so the server never sees it.
Related
Redact a PDF properly before sharing it · Can you revoke an S3 presigned URL? · Removed someone but they can still see the file · How to revoke a JWT before it expires · HTTP 401 vs 403 · no-cache vs no-store · JWT decoder · Wirebook: does the old link still work? · Fusenote: one-time notes
Questions
What is a capability URL?
A link that grants access just by being known, like a share link. It works like a password in a URL, so it must be long, random, kept out of logs and referrers, and revocable.
How long should a share link token be?
At least 128 bits of cryptographic randomness, for example 22 URL-safe Base64 characters. Never base it on an ID, timestamp or counter.
Should I store share tokens in plain text?
No. Store a hash (SHA-256 is fine for high-entropy random tokens) and compare hashes, so a database leak does not hand out working links.
Why not just use presigned storage URLs as share links?
They cannot be revoked one by one and their rules are fixed when signed. Use them only as a short redirect after your own permission check.
Which status code for a revoked link?
404 or 410 Gone if the link should simply stop existing; 403 if a signed-in user is not allowed; 401 if they need to sign in first.
Published · updated .