Noizu.MCP.Auth.Server.Upstream behaviour (Noizu MCP v0.1.6)

Copy Markdown View Source

How the authorization server authenticates the human.

This facade owns OAuth client and token semantics; it does not own identity. Authenticating the end user is delegated to whatever the host already does — which for both of our apps is an Authentik OIDC login that already works, with sessions, MFA and account recovery already in place. Reimplementing that here would be a second, worse login.

Two implementations ship:

Whatever the implementation, the upstream credential stops here. This server mints its own tokens for a subject it resolved; it never forwards an upstream access token to an MCP client, and never forwards an MCP client's token upstream.

Summary

Types

The resolved end user. subject is what lands in the access token's sub, so it must be stable and must be what the host's own authorization checks use.

Callbacks

Who is this request from?

Handle the upstream's callback, for implementations that run their own round trip. HostSession does not need this — the host's own callback lands the user back on the authorization endpoint with a session.

Functions

Delegate to the configured implementation's authenticate/4.

Delegate to the configured implementation's callback/4.

Whether the implementation runs its own callback leg.

Resolve the configured implementation and its options.

Normalize whatever a host callback returned into an identity.

Types

identity()

@type identity() :: %{
  :subject => String.t(),
  optional(:email) => String.t() | nil,
  optional(:name) => String.t() | nil,
  optional(:claims) => map()
}

The resolved end user. subject is what lands in the access token's sub, so it must be stable and must be what the host's own authorization checks use.

Callbacks

authenticate(conn, state, t, opts)

@callback authenticate(
  conn :: term(),
  state :: String.t(),
  Noizu.MCP.Auth.Server.Config.t(),
  opts :: keyword()
) :: {:ok, identity()} | {:redirect, String.t()} | {:error, term()}

Who is this request from?

  • {:ok, identity} — authenticated; the flow continues to consent
  • {:redirect, url} — not authenticated; send them here to log in, and they come back to the authorization endpoint
  • {:error, reason} — cannot tell; the flow renders an error

Implementations must not treat an inbound Authorization header as identity — that would make the authorization endpoint accept the very tokens it issues.

callback(conn, params, t, opts)

(optional)
@callback callback(
  conn :: term(),
  params :: map(),
  Noizu.MCP.Auth.Server.Config.t(),
  opts :: keyword()
) ::
  {:ok, identity(), state :: String.t()} | {:error, term()}

Handle the upstream's callback, for implementations that run their own round trip. HostSession does not need this — the host's own callback lands the user back on the authorization endpoint with a session.

Functions

authenticate(conn, state, config)

@spec authenticate(term(), String.t(), Noizu.MCP.Auth.Server.Config.t()) ::
  {:ok, identity()} | {:redirect, String.t()} | {:error, term()}

Delegate to the configured implementation's authenticate/4.

callback(conn, params, config)

@spec callback(term(), map(), Noizu.MCP.Auth.Server.Config.t()) ::
  {:ok, identity(), String.t()} | {:error, term()}

Delegate to the configured implementation's callback/4.

handles_callback?(config)

@spec handles_callback?(Noizu.MCP.Auth.Server.Config.t()) :: boolean()

Whether the implementation runs its own callback leg.

impl(config)

Resolve the configured implementation and its options.

normalize_identity(subject)

@spec normalize_identity(term()) :: {:ok, identity()} | :error

Normalize whatever a host callback returned into an identity.

A bare string is accepted as the subject, since that is what a host bridge usually has to hand.