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

The MCP authorization flow, run by `Sake.Client` itself. Turn it on with
`auth:` on `Sake.Client.connect/2`:

    {:ok, session} =
      Sake.Client.connect("https://mcp.example.com/mcp",
        auth: [authorize: &Sake.Client.Auth.Browser.authorize/1]
      )

On a 401 the client discovers the authorization server (RFC 9728, then
RFC 8414 or OpenID Connect Discovery), registers or identifies itself,
sends the person to the authorization endpoint (PKCE with S256, a
`state`, the RFC 8707 `resource`), checks the answer (`state`, and the
RFC 9207 `iss`), exchanges the code for a token, and retries the
request with it. From then on every request carries the token; an
expired one is refreshed, a 401 tries the refresh token before the whole
flow, and a 403 `insufficient_scope` asks again for the union of the
scopes requested so far and the challenged ones (step-up), at most
twice for one request. A 401 `insufficient_user_authentication` (RFC
9470: the person must sign in more strongly, or more recently) runs
the person's grant again, the code flow or the device flow, with the
challenge's `acr_values` and `max_age`, once for one request; a
machine grant cannot, and ends with `{:error, {:auth,
{:insufficient_user_authentication, challenge}}}`.

Options:

  * `:authorize` — the interactive step: a function that receives the
    authorization URL and returns `{:ok, params}`, the query parameters
    the authorization server sent to the redirect URI (`"code"`,
    `"state"`, `"iss"`, or `"error"`), or `{:error, reason}`. Default
    `Sake.Client.Auth.Browser.authorize/1`; `Sake.Client.Auth.Headless.authorize/1`
    is the one for an authorization server that approves without a
    person.
  * `:redirect_uri` — where the authorization server sends the answer.
    Default `"http://127.0.0.1:33418/callback"`, a loopback address
    `Sake.Client.Auth.Browser` listens on.
  * `:client_id`, `:client_secret` — pre-registered credentials, used
    before anything else. `:issuer` names the authorization server they
    belong to; without it they are bound to the first one they are used
    with, and never offered to another. `:token_endpoint_auth_method`
    overrides the method chosen from the server's metadata.
  * `:client_metadata_url` — the HTTPS URL of the client's own Client ID
    Metadata Document, used as the `client_id` when the authorization
    server supports them. Its `redirect_uris` must include
    `:redirect_uri`.
  * `:client_name` — the name a dynamic registration (RFC 7591) gives;
    default the `client_info` name.
  * `:store` — `{module, arg}`, a `Sake.Client.Auth.Store` that keeps
    tokens and dynamic registrations across sessions. Without one, they
    live in the session only.
  * `:static` — `true` sends the token the `:store` holds as it is,
    for a token issued some other way (a test's, say): it is never
    refreshed, and a 401 or a 403 comes back to the caller instead of
    running the flow. Default `false`.

A person without a browser on the client's machine (a CLI over SSH, a
headless agent, a device) authorizes with the device authorization
grant (RFC 8628) in place of the code flow:

  * `grant: :device` — the client asks the authorization server's
    `device_authorization_endpoint` for a user code, shows it with
    `:device_prompt` (default `Sake.Client.Auth.Device.prompt/1`,
    which logs it; see there), and polls the token endpoint while the
    person enters the code and approves on any device where they can
    sign in. Polling honours the server's `interval`, and `slow_down`
    adds five seconds to it (RFC 8628 §3.5); a denial or an expiry
    ends it with `{:error, {:auth, :access_denied}}` or
    `{:error, {:auth, :expired_token}}`. A server whose metadata has
    no device endpoint ends it with `{:error, {:auth,
    :no_device_endpoint}}`. The tokens are refreshed as the code
    flow's are; a step-up, or a 401 the refresh token does not cure,
    runs the device flow again.

Machine grants, for a client with no person at a browser, run in place
of the authorization code flow, with the same discovery and the same
handling of expiry, 401 and step-up (the grant runs again, with the
union of the scopes, where a person would have been asked):

  * `grant: :client_credentials` — the client acts as itself (the
    `io.modelcontextprotocol/oauth-client-credentials` extension). It
    needs `:client_id` and either `:client_secret` or `:private_key`.
  * `grant: :jwt_bearer` — the client presents a JWT it was given as
    the grant (RFC 7523 §2.1), a workload identity token from its
    platform, say. `:assertion` is the JWT, or a function of
    `%{issuer: issuer, resource: resource}` returning
    `{:ok, jwt}` (to ask the platform for one with the right
    audience). `:client_id` is optional.
  * `grant: :enterprise` — enterprise-managed authorization
    (`io.modelcontextprotocol/enterprise-managed-authorization`): the
    person's ID token from the host's single sign-on is exchanged at
    the enterprise identity provider for an ID-JAG (RFC 8693), which
    the MCP authorization server takes as a JWT bearer grant. It needs
    `:client_id` (and `:client_secret` or `:private_key` when the client
    is confidential there), `:id_token` (the ID token, or a function
    like `:assertion`'s), and `:idp`: `token_endpoint:` or `issuer:` (to
    read it from the provider's metadata), and the client's
    `client_id:` and `client_secret:` at the provider
    (`token_endpoint_auth_method:` overrides the method its metadata
    suggests, `"client_secret_jwt"` included, and
    `assertion_audience:` is as below, for the provider).

    With SAML single sign-on, `:saml_assertion` (the base64url-encoded
    assertion, or a function like `:assertion`'s) replaces `:id_token`,
    and `:idp` needs `issuer:`; `idp: [saml_scope: [...]]` is the
    `scope` of the SAML exchange alone (none by default; some
    providers want `"offline_access"` before they issue a refresh
    token). The assertion is first exchanged at the
    provider for a refresh token, which is then exchanged for the
    ID-JAG (draft-ietf-oauth-identity-assertion-authz-grant §4.5). The
    refresh token is kept, in the session and the `:store`, and used for
    the next ID-JAGs until it expires or the provider refuses it; only
    then is an assertion asked for again.

DPoP (RFC 9449) binds the tokens to a key the client holds, so a token
that leaks is useless without it:

  * `:dpop` — `:auto` (the default) uses DPoP with an authorization
    server whose metadata lists ES256 in
    `dpop_signing_alg_values_supported`; `true` sends proofs to any
    server; `false` never does. It needs the optional `jose`
    dependency: without it, `:auto` never uses DPoP, and `true` raises.

With DPoP, each authorization makes a new P-256 key, kept with the
token (as PEM, in the `:store` too: a bound token is useless without
it). Every token request carries a proof made with it, and every
request to the MCP server sends the token with the `DPoP` scheme and a
fresh proof bound to it. A resource whose metadata says
`dpop_bound_access_tokens_required` gets DPoP with `:auto` whatever the
authorization server's metadata lists; when the client cannot use it
(`dpop: false`, or no `jose`), the flow stops at once with
`{:dpop_required, :dpop_off | :no_jose}`. A server that asks for a nonce
(`use_dpop_nonce`) gets the request again with its nonce, which the
client then keeps sending; that retry does not count as an
authorization.

`:private_key` is a PEM private key, or `{pem, alg}` to sign with
another algorithm than its default (RS256 for RSA, ES256 for P-256):
the client then authenticates with `private_key_jwt` (RFC 7523 §2.2),
signing a short-lived assertion for every token request. It needs the
optional `jose` dependency. `token_endpoint_auth_method:
"client_secret_jwt"` signs the same assertion with HS256 over
`:client_secret` instead (at least 32 bytes), which needs no `jose`;
it is also chosen for a server that lists it as its only secret
method. `:assertion_audience` sets the `aud` of both kinds of client
assertion: `:issuer` (the default, the server's issuer identifier, as
draft-ietf-oauth-rfc7523bis asks), `:token_endpoint` (the token
endpoint's URL, which some servers want), or a string to send as it
is. Machine grants get no refresh token: an
expired token is replaced by running the grant again.

Pushed authorization requests (RFC 9126) keep the authorization
request out of the browser:

  * `:par` — `:auto` (the default) pushes the request to the server's
    `pushed_authorization_request_endpoint` whenever its metadata
    lists one, authenticated as at the token endpoint and with a DPoP
    proof when the client uses DPoP (the code is then bound to the
    key), and opens the authorization endpoint with only the
    `client_id` and the `request_uri` it got back. `false` sends the
    request through the browser, unless the metadata says
    `require_pushed_authorization_requests`: such a server gets it
    pushed anyway. A push the server refuses ends the flow with
    `{:error, {:auth, {:par_failed, status, body}}}`.

OpenID Connect tells the client who signed in:

  * `:openid` — `true` adds the `openid` scope to the person's grants
    (the code flow and the device flow) with an authorization server
    whose metadata lists it, sends a `nonce` with an authorization
    request, and checks the `id_token` that comes back with the
    tokens: its signature (with a key from the server's `jwks_uri`),
    `iss`, `aud` (the client), `exp`, the `nonce` and `at_hash`; an ID
    token from a refresh must name the same person. An ID token that
    fails ends the flow with `{:error, {:auth, {:invalid_id_token,
    reason}}}`. `id_token_claims/1` returns the verified claims. It
    needs the optional `jose` dependency. Default `false`.

Without `auth:`, a 401 is returned as
`{:error, {:unauthorized, %Sake.Auth.Challenge{}}}` and the caller
handles it. A failure inside the flow is `{:error, {:auth, reason}}`.

# `t`

```elixir
@type t() :: %Sake.Client.Auth{
  assertion: term(),
  assertion_audience: term(),
  authorize: term(),
  client_id: term(),
  client_issuer: term(),
  client_metadata_url: term(),
  client_name: term(),
  client_secret: term(),
  clients: term(),
  device_prompt: term(),
  dpop: term(),
  grant: term(),
  id_token: term(),
  idp: term(),
  idp_token: term(),
  nonces: term(),
  openid: term(),
  par: term(),
  private_key: term(),
  proof_key: term(),
  redirect_uri: term(),
  req: term(),
  saml_assertion: term(),
  sleep: term(),
  static: term(),
  store: term(),
  token: term(),
  token_endpoint_auth_method: term(),
  url: term()
}
```

# `id_token_claims`

```elixir
@spec id_token_claims(%{auth: t() | nil}) :: {:ok, map()} | :error
```

The verified claims of the ID token of a session's authorization, with
`openid: true`: `{:ok, claims}` (`"sub"`, the person's ID at the
authorization server, `"iss"`, `"auth_time"`, ...), or `:error` when
the session holds none (no `auth:`, no authorization yet, a server
without OpenID Connect, a machine grant). For the `profile` and
`email` claims, ask the server's userinfo endpoint with the token.

---

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