MPP.Challenge (mpp v0.6.1)

Copy Markdown View Source

Payment challenge — the 402 response that tells a client what to pay.

A challenge is returned in the WWW-Authenticate: Payment header when a request lacks a valid payment credential. The challenge ID is HMAC-SHA256 bound to all challenge parameters, making it tamper-proof without requiring server-side state.

HMAC Binding

The challenge ID is computed as:

base64url(HMAC-SHA256(secret_key, realm|method|intent|request|expires|digest|opaque))

Seven fixed positional slots joined by |. Optional fields use empty string when absent, preserving slot positions. The request and opaque fields are used as their raw base64url-encoded strings (never re-serialized).

Fields

  • id — HMAC-SHA256 challenge ID (computed by create/2)
  • realm — server protection space (e.g., "api.example.com")
  • method — payment method name (e.g., "stripe", "tempo")
  • intent — intent type (e.g., "charge")
  • request — base64url-encoded JSON request payload (pre-encoded)
  • description — (optional) human-readable description
  • digest — (optional) content digest per RFC 9530
  • expires — (optional) RFC 3339 expiration timestamp
  • opaque — (optional) base64url-encoded JSON server correlation data

API Functions

FunctionArityDescriptionParam Kinds
verify2Verify a challenge's HMAC-bound ID against a secret key using constant-time comparison.challenge: value, secret_key: value
create2Create a new challenge with an HMAC-SHA256 bound ID.params: value, secret_key: value

Summary

Functions

Create a new challenge with an HMAC-SHA256 bound ID.

Verify a challenge's HMAC-bound ID against a secret key using constant-time comparison.

Types

t()

@type t() :: %MPP.Challenge{
  description: String.t() | nil,
  digest: String.t() | nil,
  expires: String.t() | nil,
  id: String.t() | nil,
  intent: String.t(),
  method: String.t(),
  opaque: String.t() | nil,
  realm: String.t(),
  request: String.t()
}

Functions

create(params, secret_key)

@spec create(
  keyword(),
  String.t()
) :: t()

Create a new challenge with an HMAC-SHA256 bound ID.

Parameters

  • params - Keyword list with :realm, :method, :intent, :request (required) and :description, :digest, :expires, :opaque (optional) (value)
  • secret_key - HMAC-SHA256 secret key for challenge binding (value)

Returns

Challenge struct with computed id (struct)

Composes With

  • verify
# descripex:contract
%{
  params: %{
    params: %{
      description: "Keyword list with `:realm`, `:method`, `:intent`, `:request` (required) and `:description`, `:digest`, `:expires`, `:opaque` (optional)",
      kind: :value
    },
    secret_key: %{
      description: "HMAC-SHA256 secret key for challenge binding",
      kind: :value
    }
  },
  returns: %{type: :struct, description: "Challenge struct with computed `id`"},
  composes_with: [:verify]
}

verify(challenge, secret_key)

@spec verify(t(), String.t()) :: :ok | {:error, :invalid_challenge}
@spec verify(t(), String.t()) :: {:error, :invalid_challenge}

Verify a challenge's HMAC-bound ID against a secret key using constant-time comparison.

Parameters

  • challenge - Challenge struct to verify (value)
  • secret_key - HMAC-SHA256 secret key used when challenge was created (value)

Returns

:ok if valid, {:error, :invalid_challenge} if tampered (tagged)

Errors

  • :invalid_challenge

Composes With

  • create
# descripex:contract
%{
  params: %{
    challenge: %{description: "Challenge struct to verify", kind: :value},
    secret_key: %{
      description: "HMAC-SHA256 secret key used when challenge was created",
      kind: :value
    }
  },
  errors: [:invalid_challenge],
  returns: %{
    type: :tagged,
    description: "`:ok` if valid, `{:error, :invalid_challenge}` if tampered"
  },
  composes_with: [:create]
}