ConduitMcp.Client.OAuth.Pending (ConduitMCP v0.11.0)

Copy Markdown View Source

An authorization-code flow between the authorization request and the callback: everything the client must remember to validate the response and redeem the code.

new/1 generates the two secrets of the flow: a state of 32 random bytes (base64url, 43 characters) and a PKCE code verifier (ConduitMcp.Client.OAuth.PKCE.verifier/0), both wrapped in ConduitMcp.Client.Secret, and stamps a 10 minute TTL. It records the issuer the flow was started against (for the RFC 9207 iss check), the canonical resource, the redirect URI (the token request must repeat it exactly), the requested scopes and the principal - the end-user or session identity of the host - that the flow is bound to.

The host stores it

Pending is plain data the host keeps between the redirect and the callback: in its session store, a database row, or an encrypted cookie. to_map/1 gives a string-keyed map of strings, integers, booleans and lists (plus the principal as given) for that; from_map/1 validates and restores it. Restore the struct before calling ConduitMcp.Client.OAuth.Callback.validate/3.

The map holds secrets in plain text (state and code_verifier). Keep it server-side, or encrypt it (for example with Plug.Crypto.encrypt/4); signing alone does not hide it. inspect/1 of the struct shows both as #Secret<redacted>.

Single use is the host's job

state must be usable exactly once (OA-2). Because Pending is data, the library cannot see a second copy being presented, so a replayed callback is rejected only if the host's store enforces it: look the pending entry up by state and delete it in the same step, before validating, and on failure as well as on success. ConduitMcp.Client.OAuth.Callback.validate/3 checks the state value, principal binding, expiry and iss; it does not and cannot check consumption.

Serialization of the principal

The principal is stored in to_map/1 exactly as given. A host that JSON-encodes the map should use a string principal.

Summary

Functions

The PKCE S256 code challenge for the pending verifier.

Whether the pending authorization has expired at Unix time now.

Restores a map from to_map/1. Every field is type-checked; a missing or malformed one gives :invalid_pending with %{field: name} as data (the value itself is never put in the error). The expiry is not checked here.

Starts a pending authorization with a fresh state and code verifier.

The pending authorization as a string-keyed map for host storage. It contains state and code_verifier in plain text; see the moduledoc.

Types

t()

@type t() :: %ConduitMcp.Client.OAuth.Pending{
  client_id: String.t() | nil,
  created_at: integer(),
  expires_at: integer(),
  iss_parameter_supported: boolean(),
  issuer: String.t(),
  principal: term(),
  redirect_uri: String.t(),
  resource: String.t(),
  scopes: [String.t()],
  state: ConduitMcp.Client.Secret.t(),
  token_endpoint: String.t() | nil,
  verifier: ConduitMcp.Client.Secret.t()
}
  • :state, :verifier - the flow's secrets.
  • :redirect_uri - sent in the authorization request; must be repeated in the token request.
  • :issuer - the authorization server's issuer, as validated.
  • :resource - the canonical resource URI (RFC 8707 resource).
  • :scopes - the requested scopes.
  • :principal - the host identity the flow is bound to (nil if none).
  • :iss_parameter_supported - the AS metadata's authorization_response_iss_parameter_supported was true.
  • :client_id, :token_endpoint - the client identity and token endpoint the flow was started with, so the code is redeemed at the same authorization server (nil when not recorded).
  • :created_at, :expires_at - Unix seconds.

Functions

code_challenge(pending)

@spec code_challenge(t()) :: String.t()

The PKCE S256 code challenge for the pending verifier.

expired?(pending, now)

@spec expired?(t(), integer()) :: boolean()

Whether the pending authorization has expired at Unix time now.

from_map(map)

@spec from_map(term()) :: {:ok, t()} | {:error, ConduitMcp.Client.OAuth.Error.t()}

Restores a map from to_map/1. Every field is type-checked; a missing or malformed one gives :invalid_pending with %{field: name} as data (the value itself is never put in the error). The expiry is not checked here.

new(opts)

@spec new(keyword()) :: t()

Starts a pending authorization with a fresh state and code verifier.

Raises NimbleOptions.ValidationError on invalid options.

Options

  • :redirect_uri - Required. An https URL, or http on a loopback host (RFC 8252 §7.3); no fragment.

  • :issuer (String.t/0) - Required. The validated issuer.

  • :resource (String.t/0) - Required. The canonical resource URI.

  • :scopes (list of String.t/0) - The requested scopes. The default value is [].

  • :principal (term/0) - The host identity to bind the flow to. The default value is nil.

  • :iss_parameter_supported (boolean/0) - The AS advertises authorization_response_iss_parameter_supported: true. The default value is false.

  • :client_id - The client id used. The default value is nil.

  • :token_endpoint - The validated token endpoint. The default value is nil.

  • :ttl (pos_integer/0) - Lifetime in seconds. The default value is 600.

  • :now (integer/0) - Current Unix time in seconds (default: the system clock).

to_map(pending)

@spec to_map(t()) :: %{required(String.t()) => term()}

The pending authorization as a string-keyed map for host storage. It contains state and code_verifier in plain text; see the moduledoc.