ConduitMcp.Plugs.Auth (ConduitMCP v0.11.0)

Copy Markdown View Source

Authentication plug for MCP servers.

Provides flexible authentication strategies for protecting MCP endpoints. Supports bearer tokens, API keys, custom verification functions, and more.

Options

  • :enabled - Enable/disable authentication (default: true)

  • :strategy - Authentication strategy: :bearer_token, :api_key, :custom, or :function

  • :verify - Verification function/MFA. Signature: (credential :: String.t()) -> {:ok, user} | {:error, reason}

    The returned reason must not embed the credential

    A failure reason is written to the log (clamped and stripped of control characters via ConduitMcp.Reflect). Telemetry deliberately reports the coarse atom :invalid_credential instead, because metadata is shipped verbatim to metrics backends. Return {:error, :not_found}, not {:error, "no user for token abc123"}.

  • :token - Static token for :bearer_token strategy (simple auth). Must be a non-empty string: init/1 raises otherwise, because an empty secret would match an empty Authorization: Bearer header.

  • :api_key - Static API key for :api_key strategy. Must be a non-empty string, for the same reason.

  • :header - Header name for :api_key strategy (default: "x-api-key")

  • :assign_as - Key to assign authenticated user in conn.assigns (default: :current_user)

  • :principal_id - Explicit stable identity for a static shared secret: requires :token (:bearer_token strategy) or :api_key (:api_key strategy). A shared static credential identifies exactly one principal; without this the id is a stable digest of the credential. Setting it anywhere else — :function / :custom, or a :verify function under :bearer_token / :api_key — raises ArgumentError at init/1: one fixed id would merge every user the verifier tells apart into a single principal. Return the identity from :verify instead. See ConduitMcp.Principal for every id format.

The authenticated principal

On success this plug assigns:

  • conn.assigns[:current_user] (or :assign_as) — whatever your :verify function returned. Unchanged, and shaped however you like.
  • conn.assigns[:mcp_principal] — the canonical ConduitMcp.Principal, carrying a stable scalar :id. Task ownership and per-user rate limiting read this, never :current_user.

Read it with ConduitMcp.Principal.get/1 / ConduitMcp.Principal.id/1.

Examples

Disabled (Development)

plug ConduitMcp.Plugs.Auth, enabled: false

Static Bearer Token

plug ConduitMcp.Plugs.Auth,
  strategy: :bearer_token,
  token: "my-secret-token"

Static API Key

plug ConduitMcp.Plugs.Auth,
  strategy: :api_key,
  api_key: "secret-key-123",
  header: "x-api-key"

Custom Function (Anonymous)

plug ConduitMcp.Plugs.Auth,
  strategy: :function,
  verify: fn token ->
    if MyApp.Auth.valid_token?(token) do
      {:ok, MyApp.Auth.get_user_by_token(token)}
    else
      {:error, "Invalid token"}
    end
  end

Custom Function (MFA)

plug ConduitMcp.Plugs.Auth,
  strategy: :function,
  verify: {MyApp.Auth, :verify_token, []}  # Will call MyApp.Auth.verify_token(token)

Database Token Lookup

plug ConduitMcp.Plugs.Auth,
  strategy: :function,
  verify: fn token ->
    case MyApp.Repo.get_by(ApiToken, token: token) do
      %ApiToken{user_id: user_id} ->
        user = MyApp.Repo.get!(User, user_id)
        {:ok, user}
      nil ->
        {:error, "Invalid token"}
    end
  end

JWT Verification

plug ConduitMcp.Plugs.Auth,
  strategy: :function,
  verify: fn token ->
    case MyApp.JWT.verify_and_validate(token) do
      {:ok, claims} ->
        user = MyApp.Accounts.get_user!(claims["sub"])
        {:ok, user}
      {:error, _reason} ->
        {:error, "Invalid JWT"}
    end
  end

OAuth2 Integration

plug ConduitMcp.Plugs.Auth,
  strategy: :function,
  verify: {MyApp.OAuth, :verify_token, []},
  assign_as: :oauth_user