Fitting OAuth errors into an existing JSON error envelope

Copy Markdown View Source

The authorization-server controllers and the protected-resource plugs render errors through AttestoPhoenix.OAuthError, which owns the RFC-mandated status codes, WWW-Authenticate challenges, and cache-control semantics. An app that already has its own JSON error shape can reshape the body without losing those RFC guarantees, using three optional transport hooks in AttestoPhoenix.Config.

These hooks change only the transport rendering. The error code, the HTTP status, the challenge header, and the no-store semantics are still owned by the library.

:send_error - reshape the body

(conn, status, body_map -> conn). Called to serialize an OAuth/OIDC error into the host's envelope. Preserve status and the meaning of body_map (which carries error and, usually, error_description per RFC 6749 §5.2).

send_error: fn conn, status, body_map ->
  conn
  |> Plug.Conn.put_status(status)
  |> Phoenix.Controller.json(%{
    "ok" => false,
    # Keep the RFC fields so spec-compliant clients still parse the error.
    "error" => %{
      "code" => body_map["error"] || body_map[:error],
      "message" => body_map["error_description"] || body_map[:error_description]
    }
  })
end

Do not drop the RFC status: a token error is 400/401 for a reason (RFC 6749 §5.2), and clients branch on it.

:www_authenticate - write the challenge header

(conn, challenge_string -> conn). RFC 6749 §5.2 and RFC 6750 §3 require a matching WWW-Authenticate header on 401s. The library computes the exact challenge string (scheme + error, error_description, scope, and DPoP algs auth-params); this hook only writes it.

www_authenticate: fn conn, challenge ->
  Plug.Conn.put_resp_header(conn, "www-authenticate", challenge)
end

Write the challenge verbatim. Rewriting it risks dropping an auth-param a client needs (for example the DPoP algs).

:no_store - suppress caching

(conn -> conn). A token/credential response must never be cached (RFC 6749 §5.1). This hook applies the host's no-store headers.

no_store: fn conn ->
  Plug.Conn.put_resp_header(conn, "cache-control", "no-store")
end

What stays the library's job

  • Choosing the error code and HTTP status per the governing RFC.
  • Deciding when a WWW-Authenticate challenge or no-store header is required.
  • Computing the challenge string contents.

The hooks let you control the bytes; they do not let you change the protocol semantics.