Minting: access tokens (signed JWTs), refresh tokens and authorization codes (opaque random, hashed at rest).
No token passthrough
Everything here is minted from a subject this server resolved locally — from the host's own session, or from an upstream authorization code this server itself exchanged. An inbound bearer is never re-emitted, wrapped, or forwarded upstream. That is the MCP spec's token-passthrough prohibition, and it is a property of this module: no function takes an inbound token as input.
Audience
Every access token carries exactly one aud: the canonical resource URI of the
mount it is for. Not a list — a single string, so
Noizu.MCP.Auth.JWTVerifier can insist on an exact match and a token for
/mcp cannot open /mcp/learning.
Summary
Functions
The JWKS document — RS256 mode only. In HS256 mode there is no public key to
publish, and the metadata document omits jwks_uri accordingly.
Mint a signed access token. Returns {:ok, jwt, claims}.
Mint an authorization code bound to the PKCE challenge, the redirect URI and the resource the request asked for.
Mint a refresh token. Returns {:ok, raw_token, %RefreshToken{}}; the raw value
goes to the client, the struct to the store (which hashes it).
Resolve the HS256 secret. Accepts a binary, {mod, fun} or a 0-arity fun, so a
release reads it from runtime config rather than baking it into the build.
Sign a claim set with the configured key.
The RFC 6749 §5.1 token response body.
Record an issued access token, when track_access_tokens: true and the adapter
supports it. A no-op otherwise: the 15-minute TTL is what bounds an untracked
token.
Verifier options for a mount, derived from the same config that mints the tokens.
Types
Functions
@spec jwks(Noizu.MCP.Auth.Server.Config.t()) :: %{required(String.t()) => list()}
The JWKS document — RS256 mode only. In HS256 mode there is no public key to
publish, and the metadata document omits jwks_uri accordingly.
@spec mint_access_token(Noizu.MCP.Auth.Server.Config.t(), grant()) :: {:ok, String.t(), map()}
Mint a signed access token. Returns {:ok, jwt, claims}.
jti is random per token so a tracked token can be revoked individually.
@spec mint_authorization_code(Noizu.MCP.Auth.Server.Config.t(), map()) :: {:ok, String.t(), Noizu.MCP.Auth.Server.Store.AuthorizationCode.t()}
Mint an authorization code bound to the PKCE challenge, the redirect URI and the resource the request asked for.
All three are checked again at redemption. The refresh_family_id allocated
here is what the refresh token inherits.
@spec mint_refresh_token(Noizu.MCP.Auth.Server.Config.t(), grant()) :: {:ok, String.t(), Noizu.MCP.Auth.Server.Store.RefreshToken.t()}
Mint a refresh token. Returns {:ok, raw_token, %RefreshToken{}}; the raw value
goes to the client, the struct to the store (which hashes it).
family_id is inherited from the authorization code, so a whole rotation chain
can be revoked at once when one link is replayed.
Resolve the HS256 secret. Accepts a binary, {mod, fun} or a 0-arity fun, so a
release reads it from runtime config rather than baking it into the build.
@spec sign(Noizu.MCP.Auth.Server.Config.t(), map()) :: String.t()
Sign a claim set with the configured key.
@spec token_response( Noizu.MCP.Auth.Server.Config.t(), String.t(), [String.t()], String.t() | nil ) :: map()
The RFC 6749 §5.1 token response body.
scope is echoed always, not only when it differs from the request: a client
that asked for more than it got has to be able to see that without diffing.
@spec track_access_token(Noizu.MCP.Auth.Server.Config.t(), map(), grant()) :: :ok
Record an issued access token, when track_access_tokens: true and the adapter
supports it. A no-op otherwise: the 15-minute TTL is what bounds an untracked
token.
@spec verifier_opts(Noizu.MCP.Auth.Server.Config.t(), String.t()) :: keyword()
Verifier options for a mount, derived from the same config that mints the tokens.
Using this in MCPConfig.plug_opts/2 is what keeps the issuer, the algorithm
and the audience from drifting between the two halves — the failure mode being
tokens that verify nowhere, with nothing in the logs to say why.
auth: [verifier: {Noizu.MCP.Auth.JWTVerifier,
Noizu.MCP.Auth.Server.Tokens.verifier_opts(as_opts(), resource)}]