Mailglass.Tracking.Token (Mailglass v1.0.0)

Copy Markdown View Source

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

sign_click(endpoint, delivery_id, tenant_id, target_url)

(since 0.1.0)
@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.

sign_open(endpoint, delivery_id, tenant_id)

(since 0.1.0)
@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.

verify_click(endpoint, token)

(since 0.1.0)
@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).

verify_open(endpoint, token)

(since 0.1.0)
@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.