%%%------------------------------------------------------------------- %%% @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.