Noizu.MCP.Auth.Server.Client (Noizu MCP v0.1.6)

Copy Markdown View Source

An OAuth client of this authorization server, and the validation that admits one.

Three kinds, because MCP hosts meet clients three ways:

  • :registered — arrived through RFC 7591 dynamic registration. What Claude Desktop and Claude Code do by default.
  • :cimd — its client_id is an https URL serving a client-id metadata document. Nothing is stored until the document is fetched, and the fetch is SSRF-guarded (Noizu.MCP.Auth.Server.CIMD).
  • :preconfigured — created by the operator out of band. The only kind that may skip the consent screen, because a human already approved it.

Secrets

secret_hash is a PBKDF2 string from Noizu.MCP.Auth.Server.Secret. The plaintext exists exactly once, in the registration response; it is never stored and cannot be recovered. A client whose token_endpoint_auth_method is "none" has no secret at all — which is the common case, since a desktop app cannot keep one, and PKCE rather than a secret is what binds the code to the caller.

Summary

Functions

True when the client is not disabled.

Token-endpoint authentication methods this server supports.

Authenticate a client at the token endpoint.

True when a cached CIMD document has gone stale and should be re-fetched.

Build a client from a fetched client-id metadata document.

Build a client from an RFC 7591 registration request.

Grant types this server supports.

Client kinds.

True for a client that keeps no secret.

True when this client must be consented to before any IdP redirect or code issuance.

Response types this server supports.

True when the client registered for a grant type.

The RFC 7591 §3.2.1 registration response body. secret is the plaintext from from_registration/2, or nil for a public client.

Types

kind()

@type kind() :: :registered | :cimd | :preconfigured

t()

@type t() :: %Noizu.MCP.Auth.Server.Client{
  cimd_etag: String.t() | nil,
  cimd_expires_at: DateTime.t() | nil,
  cimd_fetched_at: DateTime.t() | nil,
  client_id: String.t(),
  client_id_kind: kind(),
  client_name: String.t() | nil,
  client_uri: String.t() | nil,
  disabled_at: DateTime.t() | nil,
  grant_types: [String.t()],
  inserted_at: DateTime.t() | nil,
  logo_uri: String.t() | nil,
  metadata: map(),
  policy_uri: String.t() | nil,
  redirect_uris: [String.t()],
  response_types: [String.t()],
  scope: [String.t()],
  secret_hash: String.t() | nil,
  software_id: String.t() | nil,
  software_version: String.t() | nil,
  token_endpoint_auth_method: String.t(),
  tos_uri: String.t() | nil,
  updated_at: DateTime.t() | nil,
  upstream_client_ref: String.t() | nil
}

Functions

active?(client)

@spec active?(t()) :: boolean()

True when the client is not disabled.

auth_methods()

@spec auth_methods() :: [String.t()]

Token-endpoint authentication methods this server supports.

authenticate(client, presented, opts \\ [])

@spec authenticate(t(), String.t() | nil, keyword()) ::
  :ok | {:error, Noizu.MCP.Auth.Server.Errors.t()}

Authenticate a client at the token endpoint.

The comparison is constant-time, and a client with no stored secret still burns the same work, so "no such client" is not measurably faster than "wrong secret".

A "none" client authenticates by presenting no secret at all — and by PKCE, which every client must satisfy. Presenting a secret for a "none" client is an error rather than something to ignore: it means the caller has the wrong idea about who it is.

cimd_stale?(client, now)

@spec cimd_stale?(t(), DateTime.t()) :: boolean()

True when a cached CIMD document has gone stale and should be re-fetched.

from_cimd(client_id, document, config)

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

Build a client from a fetched client-id metadata document.

client_id is the URL the document was fetched from. The document must assert the same client_id (RFC-draft CIMD §client_id self-assertion): without that check, one URL could serve a document claiming to be another client and inherit its consent.

A CIMD client is always public — there is no registration step in which a secret could be exchanged.

from_registration(params, config)

@spec from_registration(map(), Noizu.MCP.Auth.Server.Config.t()) ::
  {:ok, t(), String.t() | nil} | {:error, Noizu.MCP.Auth.Server.Errors.t()}

Build a client from an RFC 7591 registration request.

Returns {:ok, client, plaintext_secret | nil} — the plaintext is returned only here, for the response body, and is never persisted.

Validation is deliberately strict: redirect_uris is required and each entry must pass RedirectURI.validate/2 (https, or loopback http; no fragment, no userinfo, no wildcard), and with dcr: [allowed_redirect_hosts: [...]] the host must match on a label boundary — otherwise anyone can register https://evil-claude.ai/cb and be handed codes meant for claude.ai.

grant_types()

@spec grant_types() :: [String.t()]

Grant types this server supports.

kinds()

@spec kinds() :: [kind()]

Client kinds.

public?(client)

@spec public?(t()) :: boolean()

True for a client that keeps no secret.

requires_consent?(client, config)

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

True when this client must be consented to before any IdP redirect or code issuance.

Registered and CIMD clients always must: nobody vetted them, and skipping consent for a self-registered client is exactly the confused-deputy hole the MCP spec calls out. A preconfigured client may skip it, since an operator created it deliberately — and only then does consent: [enabled: false] apply.

response_types()

@spec response_types() :: [String.t()]

Response types this server supports.

supports_grant?(client, grant)

@spec supports_grant?(t(), String.t()) :: boolean()

True when the client registered for a grant type.

to_registration_response(client, secret)

@spec to_registration_response(t(), String.t() | nil) :: map()

The RFC 7591 §3.2.1 registration response body. secret is the plaintext from from_registration/2, or nil for a public client.