%%%-------------------------------------------------------------------
%%% @doc Authorization behaviour for `barrel_mcp_client'.
%%%
%%% The HTTP transport calls into this module to obtain the bearer
%%% token to attach to outgoing requests, and to refresh the token
%%% when the server returns 401.
%%%
%%% A handle is an opaque term passed back into every callback. Static
%%% bearer tokens use `barrel_mcp_client_auth_bearer'; OAuth 2.1 with
%%% PKCE will use `barrel_mcp_client_auth_oauth' (Phase D).
%%% @end
%%%-------------------------------------------------------------------
-module(barrel_mcp_client_auth).
-export([new/1, header/1, refresh/2]).
-export_type([t/0, handle/0]).
-type handle() :: term().
-type t() :: {module(), handle()} | none.
%% Build the auth handle from a config term.
%% `none' — no auth header sent.
%% `{bearer, Token}' — static bearer token.
%% `{oauth, Config}' — OAuth 2.1 + PKCE (authorization_code grant).
%% `{oauth_client_credentials, Config}' — OAuth 2.1 client_credentials
%% grant (RFC 6749, plus the MCP `ext-auth' OAuth Client
%% Credentials extension). For unattended agent hosts. Config
%% requires `token_endpoint' and `client_id'; supply either
%% `client_secret' or `client_assertion' (private_key_jwt).
%% `{oauth_enterprise, Config}' — Enterprise-Managed Authorization
%% (MCP `ext-auth' EMA). Chains an IdP-issued ID Token through
%% RFC 8693 token-exchange and RFC 7523 jwt-bearer to mint a
%% short-lived MCP access token. For SSO-driven hosts. Config
%% requires `idp_token_endpoint', `as_token_endpoint',
%% `client_id', `subject_token', `subject_token_type',
%% `audience', `resource'.
-callback init(Config :: term()) -> {ok, handle()} | {error, term()}.
%% Return the value to put in the `Authorization' header.
%% Returning `none' means do not attach an Authorization header.
-callback header(handle()) ->
{ok, binary()} | none | {error, term()}.
%% Refresh the credential after a 401. `WwwAuthenticate' is the
%% raw header value returned by the server, used by OAuth flows for
%% protected-resource-metadata discovery.
-callback refresh(handle(), WwwAuthenticate :: binary() | undefined) ->
{ok, handle()} | {error, term()}.
%%====================================================================
%% Helpers
%%====================================================================
%% @doc Construct an auth handle from a user-facing config term.
-spec new(
none
| {bearer, binary()}
| {oauth, map()}
| {oauth_client_credentials, map()}
| {oauth_enterprise, map()}
) ->
t() | {error, term()}.
new(none) ->
none;
new({bearer, Token}) when is_binary(Token) ->
case barrel_mcp_client_auth_bearer:init(Token) of
{ok, H} -> {barrel_mcp_client_auth_bearer, H};
Err -> Err
end;
new({oauth, Config}) when is_map(Config) ->
case barrel_mcp_client_auth_oauth:init(Config) of
{ok, H} -> {barrel_mcp_client_auth_oauth, H};
Err -> Err
end;
new({oauth_client_credentials, Config}) when is_map(Config) ->
Cfg = Config#{grant_type => client_credentials},
case barrel_mcp_client_auth_oauth:init(Cfg) of
{ok, H} -> {barrel_mcp_client_auth_oauth, H};
Err -> Err
end;
new({oauth_enterprise, Config}) when is_map(Config) ->
Cfg = Config#{grant_type => enterprise_managed},
case barrel_mcp_client_auth_oauth:init(Cfg) of
{ok, H} -> {barrel_mcp_client_auth_oauth, H};
Err -> Err
end.
%% @doc Lookup the Authorization header for the current state.
-spec header(t()) -> {ok, binary()} | none | {error, term()}.
header(none) -> none;
header({Mod, H}) -> Mod:header(H).
%% @doc Refresh after a 401, returning a new handle.
-spec refresh(t(), binary() | undefined) -> {ok, t()} | {error, term()}.
refresh(none, _) ->
{error, no_auth_configured};
refresh({Mod, H}, Www) ->
case Mod:refresh(H, Www) of
{ok, H1} -> {ok, {Mod, H1}};
Err -> Err
end.