We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Signed URLs

A signed URL contains a temporary bearer credential. Anyone who has the complete URL can use it to access a resource until it expires, even without a session cookie.

That makes signed URLs convenient for handing an authorized download to a file server or storage service... but it also means the URL must be short-lived and tightly scoped.

Creating a Signed URL

Signed URLs typically follow this flow:

  1. An authenticated user asks for a download URL
  2. The route verifies that the requester owns the file or has a support/admin role.
  3. The route returns or redirects to a signed URL
  4. The signed endpoint validates the URL
  5. If valid, the signed endpoint streams the file to the requester

Signed URL Structure

A signed URL contains the resource path, an expiration timestamp, and a signature. With dummy values, the complete URL looks like this:

http://localhost:3000/files/1/signed-download?expires=1300&signature=15a4b83da8db92f7d83ac2684ebe7d1a98108b165d4d18fbae94098ae291e5f2
  • /files/1/signed-download identifies the exact resource and action.
  • expires=1300 is an illustrative Unix timestamp.
  • signature=... is a 64-character hexadecimal HMAC over the method, path, and timestamp.

The signature is a Hash-based Message Authentication Code (HMAC), created with createHmac:

const payload = `GET\n/files/${fileId}/signed-download\n${expires}`;
const signature = createHmac("sha256", signingKey)
  .update(payload)
  .digest("hex");

The \n characters separate fields in the payload being signed. They do not appear in the URL. The secret key turns that payload into a signature, so changing the file ID, path, or expiration invalidates it.

Validating a Signed URL

The signed endpoint, /files/1/signed-download in this case, should reject malformed or expired timestamps, recompute the expected HMAC, and compare signatures with timingSafeEqual:

if (expires <= now || !timingSafeEqual(expectedSignature, providedSignature)) {
  res.status(403).send("Download Link Unavailable");
  return;
}

This verification step is what prevents an attacker from tampering with the URL. If an attacker manually changes the expiration in the URL, the signature will no longer match and the request will be rejected.

Keep expiration short! Signed URLs are bearer credentials, so if an attacker gets hold of a signed URL, they can use it until it expires. Five minutes is a reasonable limit for many use cases.

Assignment

Bearly Secure already issues five-minute, file-scoped signed URLs, but its signing helper is deliberately incomplete. Use Node's crypto APIs to sign and verify each URL.

    1. GET\n/files/FILE_ID/signed-download\nEXPIRES
      

With Bearly Secure still running, run and submit the CLI tests from the project root.