# `Sake.Auth.External`
[🔗](https://github.com/hattoriai/sake/blob/v0.4.3/lib/sake/auth/external.ex#L1)

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: :keycloak

`Sake.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' `iss` and its metadata say it
    (Auth0's ends with a slash). The protected resource metadata
    names it in `authorization_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
      the `jwks_uri` of the issuer's OpenID Connect or RFC 8414
      metadata. Needs the optional `jose` dependency.
    * `{:introspection, opts}` — token introspection (RFC 7662) at
      `endpoint:`, authenticated as the resource with `client_id:`
      and `client_secret:` (`auth_method:` `:basic`, the default, or
      `:post`). An active answer is cached for `cache:` seconds
      (default 60, `0` turns the cache off), never past the token's
      `exp`, keyed by a digest of the token.
    * a module that implements `c: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` — the `aud` values a token must carry one of. By
    default the endpoint's own URL, the resource the client asked for
    (RFC 8707). `:any` turns the audience check off; see the security
    note in the external authorization guide before you do.
  * `:typ` — the `typ` header 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's
    `ctx.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
    in `scopes_supported` and challenges name, as for `Sake.Auth`.
  * `:resources` — the endpoint paths this server protects (with
    `:param` segments), so the protected resource metadata at each
    path names the right issuer. Needed whenever Sake knows more than
    one authorization server (`Sake.Auth` and this one, or several
    external ones).
  * `:dpop` — as for `Sake.Auth`: `true` (the default, when `jose` is
    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's
    `resource_name`.
  * `:allow_insecure` — allow `http` URLs (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.

# `t`

```elixir
@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()
}
```

# `verify_token`

```elixir
@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.

# `fetch!`

```elixir
@spec fetch!(atom()) :: t()
```

The external authorization server configured under `name`; raises when there is none.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
