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:
Noizu.MCP.Auth.Server.Upstream.HostSession— the default. The host tells us who is logged in, and where to send someone who isn't.Noizu.MCP.Auth.Server.Upstream.OIDC— a self-contained OIDC round trip, for a host with no browser session of its own.
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
@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
@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 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
@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.
@spec callback(term(), map(), Noizu.MCP.Auth.Server.Config.t()) :: {:ok, identity(), String.t()} | {:error, term()}
Delegate to the configured implementation's callback/4.
@spec handles_callback?(Noizu.MCP.Auth.Server.Config.t()) :: boolean()
Whether the implementation runs its own callback leg.
@spec impl(Noizu.MCP.Auth.Server.Config.t()) :: {module(), keyword()}
Resolve the configured implementation and its options.
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.