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 URLToken looked up on every request
ExamplesS3/GCS presigned URLs, HMAC-signed query strings, JWT in a link/s/Xk3… mapped to a row in your database
Revoke one linkNo, only by expiry, rotating keys or deleting the objectYes, instantly
Change who may open it laterNo, the rules are baked inYes, rules are checked at request time
CostNo lookupOne 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

  1. Generate at least 128 bits from a cryptographic random source (for example secrets.token_urlsafe(16) in Python or crypto.getRandomValues in the browser). Never derive it from an ID, a timestamp or a counter. The UUID and password generator shows what that randomness looks like.
  2. 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_at column. A database leak then does not leak working links.
  3. Check on every request, including downloads, thumbnails and previews, not just the page that lists the file. Expired or revoked returns 404 or 410 Gone; a signed-in user who is not allowed gets 403; a link that needs sign-in gets 401 or a sign-in redirect (see 401 vs 403).
  4. Serve privately: Cache-Control: private, no-store on 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.
  5. Do not leak the token: Referrer-Policy: no-referrer so 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).
  6. Revoke and replace: “Reset link” sets revoked_at and issues a new token; the old one fails on its next use.
  7. 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 .

Try it

Related guides

Newest guides