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

Copy Markdown View Source

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

grant()

@type grant() :: %{
  :subject => String.t(),
  :client_id => String.t(),
  optional(:scope) => [String.t()],
  optional(:resource) => String.t() | nil,
  optional(:family_id) => String.t() | nil
}

Functions

jwks(config)

@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.

mint_access_token(config, grant)

@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.

mint_authorization_code(config, request)

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.

mint_refresh_token(config, grant)

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_secret(secret)

@spec resolve_secret(term()) :: String.t()

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(config, claims)

Sign a claim set with the configured key.

token_response(config, access_token, scope, refresh_token)

@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.

track_access_token(config, claims, grant)

@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.

verifier_opts(config, resource)

@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)}]