barrel_mcp_client_auth_oauth (barrel_mcp v2.0.2)

View Source

OAuth 2.1 + PKCE authorization for barrel_mcp_client.

Implements the MCP authorization flow described in the spec and the underlying RFCs:

  • RFC 9728 — Protected Resource Metadata (PRM)
  • RFC 8414 — Authorization Server Metadata
  • RFC 7636 — PKCE (S256)
  • RFC 8707 — resource indicator on auth + token requests
  • RFC 6749 / OAuth 2.1 — authorization-code + refresh_token grants

What this module does

Two responsibilities, kept separate so hosts can mix them as they need:

  1. **Discovery helpers** that hosts use during initial token acquisition: parse WWW-Authenticate, fetch PRM, fetch AS metadata, build authorization URLs with PKCE, exchange the returned code at the token endpoint.
  2. **barrel_mcp_client_auth behaviour implementation** that attaches the Authorization: Bearer ... header on every outgoing request and refreshes the token automatically on 401 (when a refresh_token was supplied).

What this module does NOT do

The authorization-code redirect step requires a browser and a local listener to capture the callback — that's a host concern, not a library one. Hosts run the interactive step however suits them (open a URL, do a CLI device-code flow, paste a code), then pass the resulting tokens back via the {oauth, Config} tuple. The library handles refresh from there.

Config shape

   {oauth, #{
     access_token   := binary(),       %% required
     refresh_token  => binary(),       %% optional; enables refresh
     token_endpoint => binary(),       %% required if refresh_token set
     client_id      => binary(),       %% required if refresh_token set
     client_secret  => binary(),       %% optional confidential client
     resource       => binary(),       %% RFC 8707 canonical id
     scopes         => [binary()]      %% optional
   }}

Summary

Functions

Build an authorization-code+PKCE URL for the user to visit. Params must include client_id and redirect_uri; the function handles code_challenge/code_challenge_method for you given the verifier. state is generated automatically if not supplied.

Acquire an access token via the OAuth 2.1 client_credentials grant — for unattended / machine-to-machine flows where there is no human in the loop. Per the MCP ext-auth OAuth Client Credentials extension, callers may authenticate either with a client_secret (HTTP Basic, per RFC 6749) or a client_assertion (private_key_jwt per RFC 7523).

Derive the S256 code challenge for a verifier.

Fetch the Authorization Server Metadata for the given issuer URL. Tries /.well-known/oauth-authorization-server first, then falls back to /.well-known/openid-configuration.

Fetch and parse the Protected Resource Metadata document.

Exchange an authorization code for tokens.

Generate a 64-byte random URL-safe code verifier (RFC 7636).

RFC 7523 JWT Bearer access-token request. The second step of the EMA chain: present the ID-JAG to the MCP server's authorization-server token endpoint and receive a short-lived access token.

Extract the resource_metadata URL from a WWW-Authenticate header per RFC 9728. Returns undefined if not present.

Refresh an access token via the refresh_token grant.

Dynamic Client Registration ([RFC 7591][rfc7591]). Posts the supplied client metadata to the AS's registration_endpoint and returns the AS's response unchanged: typically including client_id, optionally client_secret, client_id_issued_at, client_secret_expires_at, plus any client-metadata echo the AS chose to include.

Variant of register_client/2 that accepts an options map. Currently the only option is initial_access_token (RFC 7591 section 3): an opaque bearer token issued out of band by the AS to gate registration. When present, the call adds Authorization: Bearer <token>.

RFC 8693 OAuth 2.0 Token Exchange. Used by the MCP ext-auth Enterprise-Managed Authorization extension to exchange an IdP-issued ID Token (or SAML assertion) for an Identity Assertion JWT Authorization Grant (the "ID-JAG"), scoped to a specific MCP server resource.

Types

client_credentials_config/0

-type client_credentials_config() ::
          #{grant_type := client_credentials,
            token_endpoint := binary(),
            client_id := binary(),
            client_secret => binary(),
            client_assertion => binary(),
            resource => binary(),
            scopes => [binary()]}.

config/0

-type config() ::
          #{access_token := binary(),
            refresh_token => binary(),
            token_endpoint => binary(),
            client_id => binary(),
            client_secret => binary(),
            resource => binary(),
            scopes => [binary()]} |
          client_credentials_config() |
          enterprise_managed_config().

enterprise_managed_config/0

-type enterprise_managed_config() ::
          #{grant_type := enterprise_managed,
            idp_token_endpoint := binary(),
            as_token_endpoint := binary(),
            client_id := binary(),
            client_secret => binary(),
            client_assertion => binary(),
            subject_token := binary(),
            subject_token_type := binary(),
            audience := binary(),
            resource := binary(),
            scopes => [binary()]}.

handle/0

-type handle() ::
          #h{access_token :: binary() | undefined,
             refresh_token :: binary() | undefined,
             token_endpoint :: binary() | undefined,
             client_id :: binary() | undefined,
             client_secret :: binary() | undefined,
             client_assertion :: binary() | undefined,
             resource :: binary() | undefined,
             scopes :: [binary()] | undefined,
             mode :: auth_code | client_credentials | enterprise_managed,
             idp_token_endpoint :: binary() | undefined,
             subject_token :: binary() | undefined,
             subject_token_type :: binary() | undefined,
             audience :: binary() | undefined}.

Functions

build_authorization_url(AuthEndpoint, Params)

-spec build_authorization_url(binary(), map()) -> {binary(), binary(), binary()}.

Build an authorization-code+PKCE URL for the user to visit. Params must include client_id and redirect_uri; the function handles code_challenge/code_challenge_method for you given the verifier. state is generated automatically if not supplied.

client_credentials(TokenEndpoint, Params)

-spec client_credentials(binary(), map()) -> {ok, map()} | {error, term()}.

Acquire an access token via the OAuth 2.1 client_credentials grant — for unattended / machine-to-machine flows where there is no human in the loop. Per the MCP ext-auth OAuth Client Credentials extension, callers may authenticate either with a client_secret (HTTP Basic, per RFC 6749) or a client_assertion (private_key_jwt per RFC 7523).

code_challenge(Verifier)

-spec code_challenge(binary()) -> binary().

Derive the S256 code challenge for a verifier.

discover_authorization_server(Issuer)

-spec discover_authorization_server(binary()) -> {ok, map()} | {error, term()}.

Fetch the Authorization Server Metadata for the given issuer URL. Tries /.well-known/oauth-authorization-server first, then falls back to /.well-known/openid-configuration.

discover_protected_resource(Url)

-spec discover_protected_resource(binary()) -> {ok, map()} | {error, term()}.

Fetch and parse the Protected Resource Metadata document.

exchange_code(TokenEndpoint, Params)

-spec exchange_code(binary(), map()) -> {ok, map()} | {error, term()}.

Exchange an authorization code for tokens.

gen_code_verifier()

-spec gen_code_verifier() -> binary().

Generate a 64-byte random URL-safe code verifier (RFC 7636).

header(H)

init(Cfg)

jwt_bearer(TokenEndpoint, Params)

-spec jwt_bearer(binary(), map()) -> {ok, map()} | {error, term()}.

RFC 7523 JWT Bearer access-token request. The second step of the EMA chain: present the ID-JAG to the MCP server's authorization-server token endpoint and receive a short-lived access token.

parse_www_authenticate(Header)

-spec parse_www_authenticate(binary() | undefined) -> binary() | undefined.

Extract the resource_metadata URL from a WWW-Authenticate header per RFC 9728. Returns undefined if not present.

refresh(H, Www)

refresh_token(TokenEndpoint, Params)

-spec refresh_token(binary(), map()) -> {ok, map()} | {error, term()}.

Refresh an access token via the refresh_token grant.

register_client(RegistrationEndpoint, Metadata)

-spec register_client(RegistrationEndpoint :: binary(), Metadata :: map()) ->
                         {ok, ClientInfo :: map()} | {error, term()}.

Dynamic Client Registration ([RFC 7591][rfc7591]). Posts the supplied client metadata to the AS's registration_endpoint and returns the AS's response unchanged: typically including client_id, optionally client_secret, client_id_issued_at, client_secret_expires_at, plus any client-metadata echo the AS chose to include.

Hosts that receive a fresh client_id (and client_secret, if issued) feed it into a subsequent {oauth, ...}, {oauth_client_credentials, ...}, or {oauth_enterprise, ...} connect spec. This stays a standalone exchanger; auto-wiring would require persisting credentials, which is host policy.

[rfc7591]: https://datatracker.ietf.org/doc/html/rfc7591

register_client(RegistrationEndpoint, Metadata, Opts)

-spec register_client(RegistrationEndpoint :: binary(),
                      Metadata :: map(),
                      Opts :: #{initial_access_token => binary(), _ => _}) ->
                         {ok, ClientInfo :: map()} | {error, term()}.

Variant of register_client/2 that accepts an options map. Currently the only option is initial_access_token (RFC 7591 section 3): an opaque bearer token issued out of band by the AS to gate registration. When present, the call adds Authorization: Bearer <token>.

token_exchange(TokenEndpoint, Params)

-spec token_exchange(binary(), map()) -> {ok, binary()} | {error, term()}.

RFC 8693 OAuth 2.0 Token Exchange. Used by the MCP ext-auth Enterprise-Managed Authorization extension to exchange an IdP-issued ID Token (or SAML assertion) for an Identity Assertion JWT Authorization Grant (the "ID-JAG"), scoped to a specific MCP server resource.

Returns {ok, IdJag} where IdJag is the binary token extracted from the response's access_token field, or an error describing the failure. A 4xx with invalid_grant surfaces the typed {error, subject_token_expired} (the RFC 8693 error semantic for an expired or revoked subject token).