ExMCP.Authorization.CredentialStore behaviour (ex_mcp v1.0.0-rc.8)

Copy Markdown View Source

Pluggable persistence boundary for issuer-bound OAuth credentials.

ExMCP deliberately does not ship a file-backed secret store. Applications can provide an OS keychain, encrypted database, or other appropriate adapter as either AdapterModule or {AdapterModule, adapter_state}.

Registration records are stored under a versioned key containing the exact authorization-server issuer and client ID. Adapters may maintain a separate index from a non-secret local context (normally the MCP resource URL) to that key, but fetch_registration/3 validates both the returned key and record before credentials can be reused.

Token keys include the exact issuer and client ID plus resource, audience, subject/client identity, and granted scopes. Raw access and refresh tokens are never part of a key.

Legacy records without an issuer are rejected with {:credential_migration_required, kind}. Call bind_legacy_registration/2 or bind_legacy_token/2 only after independently establishing the issuer; ExMCP never attaches an unkeyed record to the currently discovered issuer.

Adapter contract

An adapter implements this behaviour. context must be a stable, non-secret local identifier. A registration fetch returns the exact storage key with the record so ExMCP can reject a corrupt or cross-issuer index.

Summary

Functions

Explicitly binds a legacy registration after its issuer is independently verified.

Explicitly binds a legacy token after every partition field is independently verified.

Fetches and validates a registration for an exact issuer.

Fetches a token only from its complete authorization partition.

Persists a validated issuer-bound registration.

Persists a token under its complete, non-secret authorization key.

Builds the versioned persistence key for a client registration.

Builds a complete token partition key without including token material.

Types

context()

@type context() :: term()

key()

@type key() :: registration_key() | token_key()

registration_key()

@type registration_key() ::
  {:ex_mcp_oauth_credential, 1, :registration, String.t(), String.t()}

store()

@type store() :: module() | {module(), term()}

token_key()

@type token_key() ::
  {:ex_mcp_oauth_credential, 1, :token, String.t(), String.t(), term(), term(),
   term(), [String.t()]}

Callbacks

fetch_registration(context, issuer, adapter_state)

@callback fetch_registration(context(), issuer :: String.t(), adapter_state :: term()) ::
  {:ok, key(), ExMCP.Authorization.CredentialStore.Registration.t() | map()}
  | :not_found
  | {:error, term()}

fetch_token(token_key, adapter_state)

@callback fetch_token(token_key(), adapter_state :: term()) ::
  {:ok, ExMCP.Authorization.CredentialStore.Token.t() | map()}
  | :not_found
  | {:error, term()}

put_registration(context, registration_key, t, adapter_state)

@callback put_registration(
  context(),
  registration_key(),
  ExMCP.Authorization.CredentialStore.Registration.t(),
  adapter_state :: term()
) :: :ok | {:error, term()}

put_token(token_key, t, adapter_state)

@callback put_token(
  token_key(),
  ExMCP.Authorization.CredentialStore.Token.t(),
  adapter_state :: term()
) ::
  :ok | {:error, term()}

Functions

bind_legacy_registration(legacy, issuer)

@spec bind_legacy_registration(map(), String.t()) ::
  {:ok, ExMCP.Authorization.CredentialStore.Registration.t()} | {:error, term()}

Explicitly binds a legacy registration after its issuer is independently verified.

bind_legacy_token(legacy, binding)

@spec bind_legacy_token(map(), map()) ::
  {:ok, ExMCP.Authorization.CredentialStore.Token.t()} | {:error, term()}

Explicitly binds a legacy token after every partition field is independently verified.

fetch_registration(store, context, issuer)

@spec fetch_registration(store(), context(), String.t()) ::
  {:ok, ExMCP.Authorization.CredentialStore.Registration.t()}
  | :not_found
  | {:error, term()}

Fetches and validates a registration for an exact issuer.

fetch_token(store, binding)

@spec fetch_token(store(), ExMCP.Authorization.CredentialStore.Token.t() | map()) ::
  {:ok, ExMCP.Authorization.CredentialStore.Token.t()}
  | :not_found
  | {:error, term()}

Fetches a token only from its complete authorization partition.

put_registration(store, context, registration)

@spec put_registration(
  store(),
  context(),
  ExMCP.Authorization.CredentialStore.Registration.t() | map()
) ::
  :ok | {:error, term()}

Persists a validated issuer-bound registration.

put_token(store, token)

@spec put_token(store(), ExMCP.Authorization.CredentialStore.Token.t() | map()) ::
  :ok | {:error, term()}

Persists a token under its complete, non-secret authorization key.

registration_key(issuer, client_id)

@spec registration_key(String.t(), String.t()) ::
  {:ok, registration_key()} | {:error, term()}

Builds the versioned persistence key for a client registration.

token_key(token_or_binding)

@spec token_key(ExMCP.Authorization.CredentialStore.Token.t() | map()) ::
  {:ok, token_key()} | {:error, term()}

Builds a complete token partition key without including token material.