An endpoint whose tokens come from an authorization server Sake does not run: your app's own OAuth provider (ExOauth2Provider, Boruta, a hand-rolled one) or an external identity provider (Keycloak, Auth0, Okta, Entra ID, Cognito, WorkOS). Sake is then only the resource server: it checks the tokens that server issues and enforces the scopes.
Name each authorization server in the Sake child, and point an
endpoint at it by name:
{Sake,
secret_key_base: ...,
external_auth: [
keycloak: [
issuer: "https://sso.example.com/realms/acme",
verify: :jwt,
servers: [MyApp.MCP]
]
]}
sake_endpoint "/mcp", server: MyApp.MCP, auth: :keycloakSake.Auth (auth: [...]) is not needed at all; with it, one endpoint
may take Sake's own tokens (auth: true) and another an external
server's.
Options
:issuer(required) — the authorization server's issuer identifier, exactly as its tokens'issand its metadata say it (Auth0's ends with a slash). The protected resource metadata names it inauthorization_servers, so MCP clients discover it as they would Sake's own.:verify(required) — how a token is checked::jwt— a JWT access token (RFC 9068), verified with the issuer's keys::jwks(a JWK Set map) or:jwks_uri, or else thejwks_uriof the issuer's OpenID Connect or RFC 8414 metadata. Needs the optionaljosedependency.{:introspection, opts}— token introspection (RFC 7662) atendpoint:, authenticated as the resource withclient_id:andclient_secret:(auth_method::basic, the default, or:post). An active answer is cached forcache:seconds (default 60,0turns the cache off), never past the token'sexp, keyed by a digest of the token.- a module that implements
verify_token/2, or a function of two arguments ({module, function}or a capture): your own code, for a provider that runs in your app.
:audiences— theaudvalues a token must carry one of. By default the endpoint's own URL, the resource the client asked for (RFC 8707).:anyturns the audience check off; see the security note in the external authorization guide before you do.:typ— thetypheader a JWT access token must have: default"at+jwt"(RFC 9068). Many identity providers issue plain"JWT"access tokens; say so, or:any.:principal— what a token stands for, as the endpoint'sctx.scope: a function of the verified claims (string keys) that returns{:ok, principal}or:error(a function of one argument, or{module, function}). By default the claims themselves.:servers— the servers whose scopes the resource metadata lists inscopes_supportedand challenges name, as forSake.Auth.:resources— the endpoint paths this server protects (with:paramsegments), so the protected resource metadata at each path names the right issuer. Needed whenever Sake knows more than one authorization server (Sake.Authand this one, or several external ones).:dpop— as forSake.Auth:true(the default, whenjoseis there),false, or[nonce: boolean, required: boolean]. A token bound to a key (cnf.jkt) is only ever accepted with a proof made with that key.:extensions— the authorization extensions to declare in the endpoint's MCP capabilities (:client_credentials,:enterprise), when this server offers their grants. Default none.:url— the origin MCP clients reach this app's endpoints at ("https://mcp.example.com"), for the resource URL the protected resource metadata names, the default audience and DPoP proofs. Without it, the origin the request arrived with: give it when a proxy in front of the app sends another host (see "Behind a proxy" in the security guide).:resource_name— the protected resource metadata'sresource_name.:allow_insecure— allowhttpURLs (issuer, keys, introspection endpoint), for development and tests.
The token
Whichever way it was verified, a token becomes the same
Sake.Auth.TokenRecord a token of Sake.Auth does, in ctx.token:
scopes from scope (or scp, a string or a list), client_id from
client_id (or azp), subject_id from sub, jkt from cnf.jkt,
expires_at from exp, acr from acr and auth_time from
auth_time (seconds since the epoch; RFC 9470: how and when the
person signed in, which a tool's authentication: requirement is
checked against), and issuer and claims besides. A token
whose cnf binds it any other way than jkt (a client certificate,
x5t#S256, RFC 8705) is refused, since Sake cannot check that
binding. A token without sub, or whose sub is its client (RFC 9068
§2.2, the client credentials grant), is a machine client's:
subject_id is nil.
Scope checks, step-up challenges, middleware, and task ownership work
as for Sake's own tokens; tasks and request states are bound to the
issuer as well as the subject, so the same sub from another issuer
is someone else.
Each refusal is the telemetry event [:sake, :auth, :external, :refused], with the configuration's name, its issuer and the
reason (:invalid_audience, :expired, :bad_signature,
:inactive, :unsupported_confirmation, ...), for finding out why a
token is not accepted.
Summary
Callbacks
Verifies a token for a provider that runs in your app. Return the
token's claims, in the shape of an introspection answer (RFC 7662
§2.2, string or atom keys): sub, client_id, scope (a string or a
list), exp (seconds or a DateTime), aud, cnf with jkt for a
DPoP-bound token, and acr and auth_time (seconds or a DateTime)
for a tool that requires a strong or recent sign-in. {:ok, claims, principal} gives the principal as
well, when the query that found the token loaded it, and skips
principal:. :error refuses the token.
Functions
The external authorization server configured under name; raises when there is none.
Types
@type t() :: %Sake.Auth.External{ allow_insecure: term(), audiences: term(), dpop: term(), extensions: term(), issuer: term(), jwks: term(), jwks_uri: term(), name: term(), principal: term(), resource_name: term(), resources: term(), secret: term(), servers: term(), typ: term(), url: term(), verify: term() }
Callbacks
@callback verify_token(token :: String.t(), conn :: Plug.Conn.t()) :: {:ok, map()} | {:ok, map(), term()} | :error
Verifies a token for a provider that runs in your app. Return the
token's claims, in the shape of an introspection answer (RFC 7662
§2.2, string or atom keys): sub, client_id, scope (a string or a
list), exp (seconds or a DateTime), aud, cnf with jkt for a
DPoP-bound token, and acr and auth_time (seconds or a DateTime)
for a tool that requires a strong or recent sign-in. {:ok, claims, principal} gives the principal as
well, when the query that found the token loaded it, and skips
principal:. :error refuses the token.