HMAC-SHA256 signing and verification for time-limited local-disk URLs.
URLs signed by this module are intended for use with
PhxMediaLibrary.Plug.MediaDownload, which calls verify/4 before serving
a file. This provides a lightweight security layer for local-disk storage
that mirrors the presigned-URL pattern already available for S3.
How It Works
A signed URL looks like:
/media/images/1/uuid/photo.jpg?sign=<token>&exp=<unix_timestamp>The token is:
Base64URL( HMAC-SHA256( secret_key_base, "<path>|<expires_at>" ) )The token and expires_at are both validated on each request:
- The expiry timestamp is checked against
System.system_time(:second). - A new token is recomputed from the path + expiry using the same secret and compared in constant time to prevent timing-based token forgery.
Configuration
Add secret_key_base and download_base_url to the disk config (or as
top-level :phx_media_library application env):
config :phx_media_library,
disks: [
local: [
adapter: PhxMediaLibrary.Storage.Disk,
root: "priv/static/uploads",
base_url: "/uploads",
download_base_url: "/media",
secret_key_base: "long-random-secret-at-least-32-bytes"
]
]In the Phoenix router, mount the plug at the same base path:
forward "/media", PhxMediaLibrary.Plug.MediaDownload, disk: :localGenerate a signed URL via PhxMediaLibrary.signed_url/3:
PhxMediaLibrary.signed_url(media)
#=> "/media/images/1/uuid/photo.jpg?sign=abc...&exp=1712345600"
PhxMediaLibrary.signed_url(media, nil, expires_in: 300)
Summary
Functions
Sign a storage path and return {token, expires_at}.
Verify a signed token for the given path.
Functions
Sign a storage path and return {token, expires_at}.
expires_at is an absolute Unix timestamp (seconds). The caller is
responsible for embedding both values in the URL query string.
Options
:secret_key_base— (required) the signing secret.:expires_in— seconds from now until the URL expires (default:3600).
Examples
iex> {token, exp} = PhxMediaLibrary.SignedUrl.sign("images/1/uuid/photo.jpg",
...> secret_key_base: "my-secret",
...> expires_in: 600
...> )
iex> is_binary(token) and is_integer(exp)
true
@spec verify(String.t(), String.t(), integer(), String.t()) :: :ok | {:error, :expired | :invalid_token}
Verify a signed token for the given path.
Returns :ok if the token is valid and has not expired, or one of:
{:error, :expired}— the URL's expiry timestamp is in the past.{:error, :invalid_token}— the HMAC does not match (wrong secret, tampered path, or tampered expiry).
Arguments
path— the storage-relative path extracted from the request (must match exactly what was signed, without leading slash).token— thesignquery param from the URL.expires_at— theexpquery param, as an integer Unix timestamp.secret— the signing secret (same value used insign/2).
Examples
iex> {token, exp} = PhxMediaLibrary.SignedUrl.sign("images/1/uuid/photo.jpg",
...> secret_key_base: "my-secret"
...> )
iex> PhxMediaLibrary.SignedUrl.verify("images/1/uuid/photo.jpg", token, exp, "my-secret")
:ok
iex> PhxMediaLibrary.SignedUrl.verify("images/1/uuid/photo.jpg", "bad-token", exp, "my-secret")
{:error, :invalid_token}