PhxMediaLibrary.SignedUrl (PhxMediaLibrary v0.6.1)

Copy Markdown View Source

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:

  1. The expiry timestamp is checked against System.system_time(:second).
  2. 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: :local

Generate 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(path, opts)

@spec sign(
  String.t(),
  keyword()
) :: {String.t(), integer()}

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

verify(path, token, expires_at, secret)

@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 — the sign query param from the URL.
  • expires_at — the exp query param, as an integer Unix timestamp.
  • secret — the signing secret (same value used in sign/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}