Behaviour for the bearer tokens Snodo.Client.HTTP sends.
A provider is passed to Snodo.Client.connect/2 as
token_provider: {module, state}. The transport calls it in the process
that makes the request:
token/2before every request, including the request that opens aSnodo.Client.listen/3stream.{:ok, token}sendsAuthorization: Bearer <token>;{:ok, nil}sends the request without one, which is how a client learns the server's challenge on its first request.refresh/3after a401, and after a403whose challenge isinsufficient_scope. It receives theSnodo.Client.Challengefrom the response (theBearerchallenge, or the first challenge of another scheme, ornilwhen the response has none) and a context with the status and the token that was refused. The provider obtains a new token, by refreshing, by starting an authorization flow, or by asking for more scope, and the transport sends the request once more with it. A second401or403returns a -32000 transport error withcause: {:unauthorized, status, challenge}.
Both callbacks return {:error, %Snodo.Error{}} to fail the request with
that error. Any other return raises ArgumentError in the caller. A token
must be a string without CR, LF, or NUL; the transport refuses one that is
not, without including it in the error.
The transport's :timeout applies to each HTTP attempt. A provider call
runs before an attempt and is not bounded by it, so a provider that
blocks, for example while a user authorizes in a browser, lengthens the
request by that long. A provider that talks to another process returns
an error, rather than exiting, when that process is not running.
The context is a map with :url, the endpoint the client connected to,
and, for refresh/3, :status and :token. A provider that keeps state
(a token cache, a refresh token, a pending authorization) typically hands
the transport a pid or a registered name and does its work in that
process, so several requesting processes share one token and one flow.
snodo_oauth supplies Snodo.OAuth.Client, which implements the MCP
authorization flows on top of this behaviour.
A token is a credential: a provider must not log it or put it in an error message.
Summary
Types
What the transport knows when it calls the provider: the endpoint URL and,
for refresh/3, the response status and the refused token (nil when
the request was sent without one).
Whatever the provider handed the transport, usually a pid or a name.
Callbacks
Returns a new token after the server refused the request with a 401,
or with a 403 insufficient_scope challenge. challenge is the parsed
challenge, or nil when the response had none; context carries the
status and the refused token. The transport sends the request once more
with the token returned.
Returns the token for the next request, {:ok, nil} to send it without
one, or {:error, error} to fail it. Called before every request and
before opening a subscriptions/listen stream.
Types
@type context() :: %{ :url => String.t(), optional(:status) => 401 | 403, optional(:token) => String.t() | nil }
What the transport knows when it calls the provider: the endpoint URL and,
for refresh/3, the response status and the refused token (nil when
the request was sent without one).
@type state() :: term()
Whatever the provider handed the transport, usually a pid or a name.
Callbacks
@callback refresh(state(), Snodo.Client.Challenge.t() | nil, context()) :: {:ok, String.t()} | {:error, Snodo.Error.t()}
Returns a new token after the server refused the request with a 401,
or with a 403 insufficient_scope challenge. challenge is the parsed
challenge, or nil when the response had none; context carries the
status and the refused token. The transport sends the request once more
with the token returned.
@callback token(state(), context()) :: {:ok, String.t() | nil} | {:error, Snodo.Error.t()}
Returns the token for the next request, {:ok, nil} to send it without
one, or {:error, error} to fail it. Called before every request and
before opening a subscriptions/listen stream.