Phoenix.Token-signed tokens for open pixel + click redirect URLs (TRACK-03, D-33..D-35).
Token shape (D-34 + D-35)
Open pixel payload: {:open, delivery_id, tenant_id}
Click redirect payload: {:click, delivery_id, tenant_id, target_url}
Open-redirect prevention (D-35 pattern a)
Target URL lives INSIDE the signed token, NEVER as a query parameter.
The class of CVE that Mailchimp shipped in 2019 + 2022
(open-redirect via weak parameter validation) is structurally
unreachable — there is no parameter to tamper with. A tampered
token fails Phoenix.Token's HMAC check → :error.
Target URL scheme is validated at SIGN time (not just verify time) —
http or https only. Attempting to sign a javascript: or ftp:
URL raises %Mailglass.ConfigError{type: :invalid}.
Salts rotation (D-33)
config :mailglass, :tracking, salts: ["q2-2026", "q1-2026"]. The
HEAD of the list signs; ALL salts in the list verify (iterate with
early return). Rotating = prepending a new salt; old salts verify
until removed from the list. Token max_age default: 2 years
(archived-email pixel loads still work).
tenant_id in payload, not URL (D-39)
Decoded tenant_id comes from the SIGNED PAYLOAD, not from URL
path/query. Phase 3 Plug uses it to call Tenancy.put_current/1.
URL path + query leak to referrer headers, shared-link screenshots,
corporate proxy logs; the signed payload is the only privacy-preserving
option.
Summary
Functions
Signs a click-redirect token. Payload:
{:click, delivery_id, tenant_id, target_url}.
Signs an open-pixel token. Payload: {:open, delivery_id, tenant_id}.
Verifies a click-redirect token. Returns
{:ok, %{delivery_id, tenant_id, target_url}} or :error.
Verifies an open-pixel token. Returns {:ok, %{delivery_id, tenant_id}}
on success or :error on any failure (expired, tampered, unknown salt).
Functions
@spec sign_click( endpoint :: Phoenix.Token.context(), String.t(), String.t(), String.t() ) :: binary()
Signs a click-redirect token. Payload:
{:click, delivery_id, tenant_id, target_url}.
Raises %Mailglass.ConfigError{type: :invalid} if target_url
scheme is not http or https.
@spec sign_open( endpoint :: Phoenix.Token.context(), delivery_id :: String.t(), tenant_id :: String.t() ) :: binary()
Signs an open-pixel token. Payload: {:open, delivery_id, tenant_id}.
Uses the HEAD of config :mailglass, :tracking, salts: to sign.
Raises %Mailglass.ConfigError{type: :missing} if no salts are configured.
@spec verify_click(endpoint :: Phoenix.Token.context(), binary()) :: {:ok, %{delivery_id: String.t(), tenant_id: String.t(), target_url: String.t()}} | :error
Verifies a click-redirect token. Returns
{:ok, %{delivery_id, tenant_id, target_url}} or :error.
Verified target_url is ALWAYS scheme ∈ ["http", "https"] (scheme was validated at sign time; tampered tokens fail HMAC first; defense-in-depth re-check at verify time per T-3-07-10).
@spec verify_open(endpoint :: Phoenix.Token.context(), binary()) :: {:ok, %{delivery_id: String.t(), tenant_id: String.t()}} | :error
Verifies an open-pixel token. Returns {:ok, %{delivery_id, tenant_id}}
on success or :error on any failure (expired, tampered, unknown salt).
Iterates over ALL configured salts to support rotation windows.