Attesto.CIBA.Request (Attesto v1.2.0)

Copy Markdown View Source

A validated CIBA backchannel authentication request (CIBA Core §7.1).

validate/3 runs the request-shape validation of the backchannel authentication endpoint on the authenticated client's wire parameters: the scope rules, the exactly-one-hint rule, the delivery-mode-dependent client_notification_token requirement, binding_message / user_code / requested_expiry shape checks, and - when the client sends a signed authentication request (§7.1.1) - JWT verification against the client's registered JWKS via Attesto.RequestObject.

It is deliberately conn-free and registry-free: client authentication (invalid_client) and the client's registered CIBA metadata are the host's concern; the host passes the latter in as the client map. What this module can NOT decide is anything requiring the resolved end-user - hint resolution (unknown_user_id, expired_login_hint_token) and user-code verification (missing_user_code, invalid_user_code) happen in the host between validate/3 and Attesto.CIBA.issue/4.

Signed authentication requests (§7.1.1, FAPI-CIBA §5.2.2)

When params carries a request JWT it MUST be the only authentication request parameter - the spec forbids parameters outside the JWT, so any other key present alongside request is rejected. (Client-authentication parameters such as client_assertion are allowed on the wire; the caller strips them before calling validate/3.) The JWT is verified with the client's registered JWKS and must carry iss (the client_id), aud (the OP's Issuer Identifier, passed as :issuer), exp, iat, nbf, and jti - all REQUIRED by §7.1.1. FAPI-CIBA additionally requires that ALL requests be signed (require_signed_request: true) and bounds the nbfexp lifetime to 60 minutes (the :max_request_lifetime_seconds default).

Each known CIBA authentication-request parameter carried in the signed JWT (scope, the hints, binding_message, user_code, acr_values, client_notification_token) MUST be a JSON string per §7.1.1; a non-string value is rejected as invalid_request rather than coerced. requested_expiry may be a string or a number.

Replay defense (FAPI-CIBA §5.2.2)

A verified signed request exposes its jti and exp on the returned struct as request_jti / request_exp (both nil for an unsigned request). The core does NOT track jti - it is stateless by design - so the host MUST record each signed request's request_jti for the request's lifetime (until request_exp) and reject a repeat at the validate/3 boundary. Without this, a captured signed authentication request can be replayed to start duplicate CIBA transactions within its lifetime.

Summary

Types

The authenticated client's registered CIBA metadata (CIBA Core §4)

Which of the three CIBA §7.1 hints the client sent, and its value.

t()

Functions

Validate the wire parameters of a backchannel authentication request for an authenticated client (CIBA Core §7.1 / §7.1.1).

Types

client()

@type client() :: %{
  :client_id => String.t(),
  :token_delivery_mode => delivery_mode(),
  optional(:jwks) => map() | [map()] | nil,
  optional(:request_signing_alg) => String.t() | nil,
  optional(:user_code_parameter) => boolean()
}

The authenticated client's registered CIBA metadata (CIBA Core §4):

  • :client_id (required) - the authenticated client.
  • :token_delivery_mode (required) - :poll | :ping | :push, the registered backchannel_token_delivery_mode. A client without one is not registered for CIBA (unauthorized_client). Note FAPI-CIBA forbids :push.

  • :jwks - the client's registered public keys, required to accept a signed authentication request.
  • :request_signing_alg - the registered backchannel_authentication_request_signing_alg; when set, a signed request must use exactly this algorithm.
  • :user_code_parameter - the registered backchannel_user_code_parameter (default false).

delivery_mode()

@type delivery_mode() :: :poll | :ping | :push

error()

@type error() ::
  :invalid_request
  | :invalid_scope
  | :invalid_binding_message
  | :unauthorized_client

hint()

@type hint() :: {:login_hint | :login_hint_token | :id_token_hint, String.t()}

Which of the three CIBA §7.1 hints the client sent, and its value.

t()

@type t() :: %Attesto.CIBA.Request{
  acr_values: [String.t()],
  binding_message: String.t() | nil,
  client_id: String.t(),
  client_notification_token: String.t() | nil,
  delivery_mode: delivery_mode(),
  hint: hint(),
  request_exp: non_neg_integer() | nil,
  request_jti: String.t() | nil,
  requested_expiry: pos_integer() | nil,
  scope: [String.t()],
  signed?: boolean(),
  user_code: String.t() | nil
}

Functions

validate(client, params, opts \\ [])

@spec validate(client(), map(), keyword()) :: {:ok, t()} | {:error, error()}

Validate the wire parameters of a backchannel authentication request for an authenticated client (CIBA Core §7.1 / §7.1.1).

params is the string-keyed parameter map with any client-authentication parameters (client_id, client_assertion, ...) already stripped by the caller. Options:

  • :issuer - the OP's Issuer Identifier, the required aud of a signed authentication request. Required to accept signed requests.
  • :require_signed_request - when true, reject a plain-parameter request (FAPI-CIBA §5.2.2 requires signed requests). Default false.
  • :accepted_algs - JOSE algorithms acceptable for signed requests. Defaults to Attesto.SigningAlg.default_client_algs/0; a FAPI-CIBA deployment narrows this to ["PS256", "ES256"]. The client's registered :request_signing_alg, when set, must be inside this set and becomes the only accepted algorithm.
  • :max_request_lifetime_seconds - bound on a signed request's nbfexp lifetime. Default 3600 (FAPI-CIBA §5.2.2's 60 minutes).
  • :user_code_supported - whether this OP advertises backchannel_user_code_parameter_supported. A user_code sent when the OP or the client's registration does not support it is rejected. Default false.
  • :binding_message_max_length - maximum binding_message length in graphemes. Default 128.
  • :require_binding_message - when true, a request without a binding_message is rejected with invalid_binding_message (FAPI-CIBA §5.2.2 requires one when no other unique authorization context exists). Default false.
  • :min_client_notification_token_length - entropy floor for ping/push client_notification_tokens, as a length lower bound. Default 22 (128 bits base64-encoded).
  • :now - unix seconds or DateTime, for signed-request time checks.

Returns {:ok, %Attesto.CIBA.Request{}} or {:error, reason} where reason is the CIBA §13 error code the endpoint renders: :invalid_request (malformed/missing parameters, or a signed request that fails verification), :invalid_scope, :invalid_binding_message, or :unauthorized_client (the client is not registered for CIBA).