Community Python Snippet
Signed URL Generator With HMAC
The 60-line HMAC-signed URL helper I use for download links and webhook callbacks. Stdlib only, constant-time verification, expiry baked in, and no S3 dependency to debug at 2 a.m.
Signed URL Generator With HMAC
The 60-line HMAC-signed URL helper I use for download links and webhook callbacks. Stdlib only, constant-time verification, expiry baked in, and no S3 dependency to debug at 2 a.m.
By @samirakumar
December 15, 2025
·
Updated May 18, 2026
604 views
8
4.5 (9)
The whole signed-URL pattern reduces to two ideas: pick an expires timestamp, then HMAC the canonical string path?expires=<ts>. I use urlsafe_b64encode and strip the trailing = padding so the signature drops cleanly into a query parameter without re-escaping. The secret is 32 random bytes generated once and stored in your secrets manager; never derive it from a username, the URL, or anything an attacker can replay. Including expires in the signed string is non-negotiable because it is the only thing stopping someone who saw one URL from minting a permanent one.
hmac.compare_digest is the part you cannot skip. A naive expected == sig comparison short-circuits on the first byte mismatch, and an attacker who can time your responses can recover the signature byte by byte across millions of requests. The verifier rebuilds the canonical string from parsed.path and the expires query param so a re-ordered URL still verifies (the canonical string is path plus ?expires=N, never the raw query). I return a tagged dict instead of a bool so the caller can log reason for ops without leaking which check failed back to the user, who only ever sees a generic 403.
Binding the HTTP method into the canonical string costs three characters and stops a whole class of mistake: an attacker who scraped a download URL out of an email cannot replay it as a DELETE against the same path. The canonical format uses newline separators (the same shape AWS SigV4 uses) so re-ordering query params cannot collide with a different signature input. I keep the format simple, two fields plus the path, because every extra slot is one more thing your verifier has to canonicalize identically on both sides. When that drifts, you get the worst kind of bug: signatures match in the unit test and fail in production because the load balancer stripped a trailing slash.
