AttestoPhoenix.DuplicateParameterGuard (AttestoPhoenix v4.2.1)

Copy Markdown View Source

Preserves and validates OAuth request parameters before Phoenix collapses them.

The raw query string remains available on Plug.Conn, so Attesto's protocol controllers reject repeated query names automatically. URL-encoded and JSON bodies are different: Plug.Parsers converts them to a map before router dispatch, losing repeated names. Configure this module as the endpoint's body reader so the controller can inspect the original bytes:

plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  pass: ["*/*"],
  json_decoder: Phoenix.json_library(),
  body_reader: {AttestoPhoenix.DuplicateParameterGuard, :read_body, []}

The reader examines the bytes before Plug.Parsers collapses them and returns every :more result unchanged so Plug can enforce its request limit. If another caller continues reading, the reader retains chunks up to the configured :length so names split across a boundary cannot bypass validation. It then keeps only the analysis until controller dispatch; validate_and_forget/1 removes that metadata before the action runs.

RFC 8707 deliberately permits more than one resource parameter. Those values in URL-encoded input are preserved as a list and passed to Attesto's resource-indicator validation. Every other repeated top-level form name is rejected. JSON object names must be unique at every nesting level. Bracketed form names are compared by their top-level root, so client_id=a&client_id[]=b cannot evade the check.

If the host already uses a custom body reader, pass its MFA as the sole extra argument:

body_reader: {
  AttestoPhoenix.DuplicateParameterGuard,
  :read_body,
  [{MyApp.BodyReader, :read_body, []}]
}

Protocol form or JSON requests without the reader emit [:attesto_phoenix, :oauth_body_guard, :missing_analysis] telemetry with a count and the HTTP method/body format. Configure oauth_body_guard: :required to reject missing analysis after installing the reader on every endpoint. Multipart requests also emit this event and are rejected under the required policy: Plug's multipart parser bypasses the body reader, so it cannot establish duplicate analysis. Host upload routes can retain multipart parsing.

Summary

Functions

A Plug.Parsers body reader that checks ambiguity before map conversion.

Types

reason()

@type reason() ::
  {:duplicate_parameter, String.t()}
  | :body_too_large
  | :invalid_parameter_encoding

Functions

read_body(conn, opts)

@spec read_body(Plug.Conn.t(), keyword()) :: tuple()

A Plug.Parsers body reader that checks ambiguity before map conversion.

It otherwise has the same return contract and read limits as Plug.Conn.read_body/2 (or the optional underlying reader MFA).

read_body(conn, opts, arg)

@spec read_body(Plug.Conn.t(), keyword(), {module(), atom(), list()}) :: tuple()