%% SPDX-FileCopyrightText: 2023 Erlang Ecosystem Foundation %% SPDX-License-Identifier: Apache-2.0 -module(oidcc_http_adapter). -hank([{unused_callbacks, [{request, 5}]}]). -moduledoc """ HTTP transport adapter. Adapters perform the transport operation for `oidcc_http_util` and return the raw response representation used by `httpc:request/5`. OIDCC remains responsible for telemetry, response decoding, content-type validation, DPoP nonce handling, and error normalization. Configure an adapter through the `request_opts` for each outbound operation: ```erlang Adapter = {my_http_adapter, #{}}, {ok, ProviderPid} = oidcc_provider_configuration_worker:start_link(#{ issuer => <<"https://my.provider">>, provider_configuration_opts => #{ request_opts => #{ http_adapter => Adapter } } }), {ok, ClientContext} = oidcc_client_context:from_configuration_worker( ProviderPid, <<"client_id">>, <<"client_secret">> ), oidcc_token:retrieve( AuthCode, ClientContext, #{ request_opts => #{ http_adapter => Adapter } } ). ``` Elixir modules can implement the same behavior: ```elixir defmodule MyHttpAdapter do @behaviour :oidcc_http_adapter @impl true def request(method, request, http_options, request_options, config) do # Return {:ok, {{http_version, status, reason}, headers, binary_body}} # or {:error, reason}. end end adapter = {MyHttpAdapter, %{}} {:ok, provider_pid} = Oidcc.ProviderConfiguration.Worker.start_link(%{ issuer: "https://my.provider", provider_configuration_opts: %{ request_opts: %{ http_adapter: adapter } } }) {:ok, client_context} = Oidcc.ClientContext.from_configuration_worker( provider_pid, "client_id", "client_secret" ) Oidcc.Token.retrieve(auth_code, client_context, %{ request_opts: %{ http_adapter: adapter } }) ``` The provider worker's `provider_configuration_opts.request_opts` apply only to provider discovery and JWKS requests. They are not copied into token, userinfo, introspection, registration, or authorization options. Configure `request_opts.http_adapter` for both paths when both need the adapter. The complete request tuple, including its original URL, is passed to the adapter unchanged. An adapter must return: * the raw status-line tuple `{HttpVersion, Status, Reason}`; * headers in the representation expected by `oidcc_http_util`; * a binary response body; or * `{error, Reason}` for a transport failure. Response header names must be lower-case byte strings as returned by `httpc`. Header values may be any iodata accepted by `httpc`. Adapters control redirect handling and response-body collection. Security-aware adapters must not follow redirects implicitly. They must resolve and validate each redirect destination before connecting, and enforce response-size limits while receiving the body. Adapters may emit their own telemetry. The telemetry span owned by `oidcc_http_util` remains authoritative for OIDCC HTTP operations. """. -export_type([config/0]). -export_type([header/0]). -export_type([http_options/0]). -export_type([method/0]). -export_type([request/0]). -export_type([request_body/0]). -export_type([request_options/0]). -export_type([response/0]). -doc "Adapter module and adapter-specific configuration.". -type config() :: {module(), map()}. -doc "HTTP method accepted by `httpc:request/5`.". -type method() :: head | get | put | patch | post | trace | options | delete. -doc "HTTP header representation accepted and returned by `httpc:request/5`.". -type header() :: {Field :: [byte()], Value :: binary() | iolist()}. -doc "HTTP request tuple accepted by `httpc:request/5`.". -type request() :: {uri_string:uri_string(), [header()]} | { uri_string:uri_string(), [header()], ContentType :: string(), request_body() }. -doc "HTTP request body accepted by `httpc:request/5`.". -type request_body() :: iolist() | binary() | { fun((Accumulator :: term()) -> eof | {ok, iolist(), Accumulator :: term()}), Accumulator :: term() } | { chunkify, fun((Accumulator :: term()) -> eof | {ok, iolist(), Accumulator :: term()}), Accumulator :: term() }. -doc """ HTTP options constructed by `oidcc_http_util`. The list includes the request timeout and, when configured, TLS options. """. -type http_options() :: [ {timeout, timeout()} | {ssl, [ssl:tls_option()]} ]. -doc "Request options passed to adapters.". -type request_options() :: [{body_format, binary}]. -doc "Raw `httpc`-compatible response.". -type response() :: {ok, { { HttpVersion :: string(), Status :: non_neg_integer(), Reason :: string() }, [header()], binary() }} | {error, term()}. -doc """ Perform an HTTP request. `Request` contains the original URL and is passed without transformation. `HttpOptions` and `RequestOptions` are the values constructed by `oidcc_http_util`. `AdapterConfig` is the map paired with the adapter module in `request_opts.http_adapter`. """. -callback request( Method :: method(), Request :: request(), HttpOptions :: http_options(), RequestOptions :: request_options(), AdapterConfig :: map() ) -> response().