# `PhoenixKit.API.MCP.Origin`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/mcp/origin.ex#L1)

The `Origin` check of the MCP transport, against DNS rebinding.

A request without an `Origin` header (every client that is not a browser)
passes. A request with one must name a host this install knows, else it is
answered `403 origin_not_allowed` before any key is looked at (the MCP plug
runs this as the `:before_auth` hook of `PhoenixKit.API.Auth`).

## What is allowed

  * the install's canonical host: the host of the `site_url` setting, else
    of the endpoint's URL (the same source as `PhoenixKit.Utils.Routes.base_url/0`,
    read through the settings cache because this check runs before any rate
    limit);
  * when the host app configures `config :phoenix_kit, :api_request_context,
    {Module, :function}`, the `:hosts` that function **explicitly returns**
    (every domain of a multi-domain install). A hook that returns no
    `:hosts` key (a hook that sets only language and currency), an empty or
    non-string list, junk, or that raises or throws contributes nothing. The
    hook is called with the conn, and it runs again later in the same request
    when the request context is resolved, so it must be cheap and free of side
    effects;
  * the entries of the `api_mcp_allowed_origins` setting, which are full
    origins: `https://client.example` matches scheme, host and port. An entry
    without `://` never matches (the Settings page stores only full origins).

The request's own `Host` header is deliberately **not** on the list. A
DNS-rebinding page sends `Host: evil.test` and `Origin: http://evil.test`;
both come from the attacker, so a list built from the `Host` header would
admit it. The default `PhoenixKit.API.RequestContext` host list is exactly
that header, and `RequestContext.resolve/1` keeps it whenever a hook returns
no `:hosts` or fails, which is why this module does not use `resolve/1` and
calls the hook itself.

An origin with userinfo, a path, a query, a fragment, a scheme other than
`http` or `https`, no host (`null`, `file://`), surrounding whitespace or
bytes that are not valid UTF-8 is never allowed. A request with two `Origin`
headers is refused.

# `allowed?`

```elixir
@spec allowed?(String.t(), [String.t()], [String.t()]) :: boolean()
```

Whether `origin` (the value of an `Origin` header) is allowed, given the
`hosts` this install serves and the `configured` entries of
`api_mcp_allowed_origins`.

# `canonical_origin`

```elixir
@spec canonical_origin() :: String.t()
```

The canonical origin, `scheme://host[:port]`: that of the `site_url` setting
(read through the settings cache), else of the endpoint's URL, else of the
static configuration, else `http://localhost:4000`. A rung that is set but
unusable (no scheme, not `http(s)`, no host) is skipped. The host is
lowercase and a default port is left out.

The last two rungs mean the install never said what its public address is:
that is logged once per boot. It never raises.

The same source as `PhoenixKit.Utils.Routes.base_url/0`, but without its
uncached database read, so a public request can use it.

# `check`

```elixir
@spec check(Plug.Conn.t()) :: Plug.Conn.t()
```

Passes `conn` when it has no `Origin` header or an allowed one; otherwise
sends `403 origin_not_allowed` and halts.

# `known_hosts`

```elixir
@spec known_hosts(Plug.Conn.t()) :: [String.t()]
```

The hostnames this install is known to serve, lowercase and without ports:
the canonical host first, then the hosts the `:api_request_context` hook
explicitly returns (see the moduledoc). The request's own `Host` header is
never on the list.

Public for the other documents the install publishes about itself
(`PhoenixKit.API.PublicDiscovery`), which must not trust the `Host` header
either.

# `origin_with_host`

```elixir
@spec origin_with_host(String.t(), String.t()) :: String.t()
```

`origin` (as `canonical_origin/0` returns it) with `host` in place of its
host: the scheme and the port are kept.

---

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