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_credentialinstead, because metadata is shipped verbatim to metrics backends. Return{:error, :not_found}, not{:error, "no user for token abc123"}.:token- Static token for:bearer_tokenstrategy (simple auth). Must be a non-empty string:init/1raises otherwise, because an empty secret would match an emptyAuthorization: Bearerheader.:api_key- Static API key for:api_keystrategy. Must be a non-empty string, for the same reason.:header- Header name for:api_keystrategy (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_tokenstrategy) or:api_key(:api_keystrategy). 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:verifyfunction under:bearer_token/:api_key— raisesArgumentErroratinit/1: one fixed id would merge every user the verifier tells apart into a single principal. Return the identity from:verifyinstead. SeeConduitMcp.Principalfor every id format.
The authenticated principal
On success this plug assigns:
conn.assigns[:current_user](or:assign_as) — whatever your:verifyfunction returned. Unchanged, and shaped however you like.conn.assigns[:mcp_principal]— the canonicalConduitMcp.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: falseStatic 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
endCustom 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
endJWT 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
endOAuth2 Integration
plug ConduitMcp.Plugs.Auth,
strategy: :function,
verify: {MyApp.OAuth, :verify_token, []},
assign_as: :oauth_user