ExMCP.Authorization.JWT (ex_mcp v1.0.0-rc.8)

Copy Markdown View Source

General-purpose JWT module wrapping JOSE for MCP authorization.

Provides key loading, JWT signing, verification, and claims validation used by OAuth client assertions, ID-JAG tokens, and JWT bearer grants.

Summary

Functions

Fetches a JWKS (JSON Web Key Set) from a URL.

Generates an EC key pair for development/testing.

Generates a unique JWT ID (jti).

Generates an RSA key pair for development/testing.

Loads a JWK from a PEM string, JWK map, or file path.

Reads the unverified header from a JWS token (for typ checking).

Signs a claims map into a compact JWS string.

Converts a JWK to a map representation (for JWKS publishing).

Extracts the public key from a JWK.

Validates standard JWT claims against expected values.

Verifies the signature of a JWS string and returns the decoded claims.

Verifies a JWS string and validates claims against expected values.

Functions

fetch_jwks(url, opts \\ [])

@spec fetch_jwks(
  String.t(),
  keyword()
) :: {:ok, [JOSE.JWK.t()]} | {:error, term()}

Fetches a JWKS (JSON Web Key Set) from a URL.

Uses the same HTTPS-only, public-address, pinned and bounded fetch policy as authorization-server and client metadata discovery. Custom HTTP clients must implement get(uri, approved_address, opts).

generate_ec_key(opts \\ [])

@spec generate_ec_key(keyword()) :: JOSE.JWK.t()

Generates an EC key pair for development/testing.

Options

  • :curve - EC curve name (default: "P-256")

generate_jti()

@spec generate_jti() :: String.t()

Generates a unique JWT ID (jti).

generate_rsa_key(opts \\ [])

@spec generate_rsa_key(keyword()) :: JOSE.JWK.t()

Generates an RSA key pair for development/testing.

Options

  • :size - Key size in bits (default: 2048)

load_key(jwk_map)

@spec load_key(map() | String.t() | {:pem_file, String.t()}) ::
  {:ok, JOSE.JWK.t()} | {:error, term()}

Loads a JWK from a PEM string, JWK map, or file path.

Examples

{:ok, jwk} = JWT.load_key(%{"kty" => "RSA", ...})
{:ok, jwk} = JWT.load_key("-----BEGIN RSA PRIVATE KEY-----\n...")
{:ok, jwk} = JWT.load_key({:pem_file, "/path/to/key.pem"})

peek_header(token)

@spec peek_header(String.t()) :: {:ok, map()} | {:error, term()}

Reads the unverified header from a JWS token (for typ checking).

sign(claims, jwk, opts \\ [])

@spec sign(map(), JOSE.JWK.t(), keyword()) :: {:ok, String.t()} | {:error, term()}

Signs a claims map into a compact JWS string.

Options

  • :alg - Signing algorithm (default: "RS256")
  • :kid - Key ID to include in header
  • :typ - Token type header (default: "JWT")

to_map(jwk)

@spec to_map(JOSE.JWK.t()) :: map()

Converts a JWK to a map representation (for JWKS publishing).

to_public_key(jwk)

@spec to_public_key(JOSE.JWK.t()) :: JOSE.JWK.t()

Extracts the public key from a JWK.

validate_claims(claims, expected \\ [])

@spec validate_claims(
  map(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Validates standard JWT claims against expected values.

Time-based claims

exp is required by default and must be a number: a token without an expiry, or with a non-numeric expiry, is rejected. nbf and iat are optional, but when present they must be numeric — a non-numeric value is rejected rather than ignored.

All time comparisons allow :leeway seconds of clock skew (default 30).

Options

  • :leeway - Clock skew allowance in seconds (default: 30)
  • :require_exp - Require an exp claim (default: true)
  • :iss - Expected issuer
  • :aud - Expected audience (string or list)
  • :sub - Expected subject
  • :max_age - Maximum token age in seconds
  • :required - List of required claim keys (as strings)

Errors

  • {:error, :missing_exp} - no exp claim and require_exp is true
  • {:error, {:invalid_claim_type, "exp" | "nbf" | "iat"}} - claim present but not a number

  • {:error, :token_expired} / {:error, :token_not_yet_valid} / {:error, :invalid_iat}

verify(token, jwk_or_jwks)

@spec verify(String.t(), JOSE.JWK.t() | [JOSE.JWK.t()]) ::
  {:ok, map()} | {:error, term()}

Verifies the signature of a JWS string and returns the decoded claims.

Accepts a single JWK or a list of JWKs (JWKS).

Signature verification only

This function performs no claims validation: an expired token, a token that is not yet valid, or a token issued for another audience all verify successfully as long as the signature is good. Use verify_and_validate/3 (or validate_claims/2 on the result) for anything that makes an authorization decision.

verify_and_validate(token, jwk_or_jwks, expected \\ [])

@spec verify_and_validate(String.t(), JOSE.JWK.t() | [JOSE.JWK.t()], keyword()) ::
  {:ok, map()} | {:error, term()}

Verifies a JWS string and validates claims against expected values.

Takes the same options as validate_claims/2. Note that exp is required by default; pass require_exp: false for the rare token profile that legitimately omits it.