NoWayJose (NoWayJose v1.0.2)

View Source

JWT signing and verification with unified key handling.

Core API

NoWayJose provides a simple, unified API for JWT operations:

Importing Keys

# From PEM (requires algorithm)
{:ok, key} = NoWayJose.import(pem_data, :pem, alg: :rs256, kid: "key-1")

# From DER (requires algorithm)
{:ok, key} = NoWayJose.import(der_data, :der, alg: :es256)

# From JWK (algorithm inferred, verification-only)
{:ok, key} = NoWayJose.import(jwk_json, :jwk)

# From JWKS (returns list of keys)
{:ok, keys} = NoWayJose.import(jwks_json, :jwks)

Generating Keys

# RSA keys
{:ok, key} = NoWayJose.generate(:rs256)
{:ok, key} = NoWayJose.generate(:rs256, bits: 4096, kid: "my-key")

# EC keys
{:ok, key} = NoWayJose.generate(:es256, kid: "ec-key")

Exporting Keys

# Export as JWK JSON (public key only)
{:ok, jwk_json} = NoWayJose.export(key, :jwk)

Signing

{:ok, token} = NoWayJose.sign(key, %{"sub" => "user123"})

Verification

{:ok, claims} = NoWayJose.verify(key, token, aud: "my-app")

JWKS Fetchers

For external identity providers, use fetchers to automatically refresh keys:

# Start a fetcher
:ok = NoWayJose.start_jwks_fetcher("auth0",
  "https://example.auth0.com/.well-known/jwks.json"
)

# Verify using stored keys
{:ok, claims} = NoWayJose.verify_with_stored(token, "auth0", aud: "my-app")

JWKS Export

Export stored keys as JWKS JSON for .well-known/jwks.json:

jwks_json = NoWayJose.export_jwks("my-app")

Summary

Types

Supported algorithms

A map containing the claims to be encoded

JSON Web Token

Functions

Decodes a JWT header without verifying the signature.

Same as decode_header/1, but raises on error.

Removes all keys for a namespace.

Exports a key to the specified format.

Exports stored keys as JWKS JSON.

Generates a new key pair.

Retrieves a key from the key store.

Retrieves all keys for a namespace.

Imports a key from PEM, DER, or JWK format.

Stores a key in the key store.

Signs claims with a key.

Same as sign/3, but raises on error.

Starts a JWKS fetcher for an external endpoint.

Stops a JWKS fetcher.

Verifies a token with a key.

Same as verify/3, but raises on error.

Verifies a token using stored keys.

Types

alg()

@type alg() ::
  :rs256 | :rs384 | :rs512 | :es256 | :es384 | :ps256 | :ps384 | :ps512 | :eddsa

Supported algorithms

claims()

@type claims() :: %{required(String.t()) => term()}

A map containing the claims to be encoded

token()

@type token() :: String.t()

JSON Web Token

validation_option()

@type validation_option() ::
  {:validate_exp, boolean()}
  | {:validate_nbf, boolean()}
  | {:leeway, non_neg_integer()}
  | {:iss, String.t() | [String.t()]}
  | {:aud, String.t() | [String.t()]}
  | {:sub, String.t()}
  | {:required_claims, [String.t()]}

validation_options()

@type validation_options() :: [validation_option()]

Functions

decode_header(token)

@spec decode_header(token()) :: {:ok, NoWayJose.Header.t()} | {:error, atom()}

Decodes a JWT header without verifying the signature.

Useful for extracting the kid to look up the correct key.

Examples

{:ok, header} = NoWayJose.decode_header(token)
# => %NoWayJose.Header{alg: "RS256", typ: "JWT", kid: "key-1"}

decode_header!(token)

@spec decode_header!(token()) :: NoWayJose.Header.t() | no_return()

Same as decode_header/1, but raises on error.

delete_keys(name)

@spec delete_keys(String.t()) :: :ok

Removes all keys for a namespace.

Examples

:ok = NoWayJose.delete_keys("my-app")

export(key, atom)

@spec export(NoWayJose.Key.t(), :jwk | :pem | :der) ::
  {:ok, String.t() | binary()} | {:error, atom()}

Exports a key to the specified format.

Formats

  • :jwk - Export as JWK JSON (public key only)
  • :pem - Export as PEM string (public key only)
  • :der - Export as DER binary (public key only)

Note: JWK-imported keys can only be exported as JWK.

Examples

{:ok, jwk_json} = NoWayJose.export(key, :jwk)
{:ok, pem_string} = NoWayJose.export(key, :pem)
{:ok, der_binary} = NoWayJose.export(key, :der)

export_jwks(name)

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

Exports stored keys as JWKS JSON.

Only public key components are exported - private key material is never included.

Examples

# Export keys for a namespace
jwks_json = NoWayJose.export_jwks("my-app")
# => ~s({"keys":[{"kty":"RSA","kid":"key-1","n":"...","e":"AQAB"}]})

# Serve at .well-known/jwks.json
get "/.well-known/jwks.json" do
  send_resp(conn, 200, NoWayJose.export_jwks("my-app"))
end

generate(alg, opts \\ [])

@spec generate(
  alg(),
  keyword()
) :: {:ok, NoWayJose.Key.t()} | {:error, atom()}

Generates a new key pair.

Algorithm determines key type:

  • RSA: :rs256, :rs384, :rs512, :ps256, :ps384, :ps512
  • EC: :es256 (P-256), :es384 (P-384)

Options

  • :bits - RSA key size (default: 2048, ignored for EC)
  • :kid - Key identifier (optional)

Examples

{:ok, key} = NoWayJose.generate(:rs256)
{:ok, key} = NoWayJose.generate(:rs256, bits: 4096, kid: "my-key")
{:ok, key} = NoWayJose.generate(:es256, kid: "ec-key")

get_key(name, kid)

@spec get_key(String.t(), String.t() | nil) :: {:ok, NoWayJose.Key.t()} | :error

Retrieves a key from the key store.

Examples

{:ok, key} = NoWayJose.get_key("my-app", "key-1")

get_keys(name)

@spec get_keys(String.t()) :: [NoWayJose.Key.t()]

Retrieves all keys for a namespace.

Examples

keys = NoWayJose.get_keys("my-app")

import(data, format, opts \\ [])

@spec import(binary(), :pem | :der | :jwk | :jwks, keyword()) ::
  {:ok, NoWayJose.Key.t()} | {:ok, [NoWayJose.Key.t()]} | {:error, atom()}

Imports a key from PEM, DER, or JWK format.

Formats

  • :pem - PEM-encoded key (requires alg option)
  • :der - DER-encoded key (requires alg option)
  • :jwk - JWK JSON (alg inferred from JWK)
  • :jwks - JWKS JSON (returns list of keys)

Options

  • :alg - Algorithm (required for PEM/DER): :rs256, :rs384, :rs512, :es256, :es384, etc.
  • :kid - Key identifier (optional)

Examples

{:ok, key} = NoWayJose.import(pem, :pem, alg: :rs256, kid: "key-1")
{:ok, key} = NoWayJose.import(jwk_json, :jwk)
{:ok, keys} = NoWayJose.import(jwks_json, :jwks)

Notes

JWK-imported keys are verification-only (jsonwebtoken limitation).

put_key(name, key)

@spec put_key(String.t(), NoWayJose.Key.t()) :: :ok

Stores a key in the key store.

Examples

:ok = NoWayJose.put_key("my-app", key)

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

@spec sign(NoWayJose.Key.t(), claims(), keyword()) ::
  {:ok, token()} | {:error, atom()}

Signs claims with a key.

Options

  • :kid - Override the key ID in the JWT header (optional)

Examples

claims = %{"sub" => "user123", "aud" => "my-app"}
{:ok, token} = NoWayJose.sign(key, claims)

# With custom kid
{:ok, token} = NoWayJose.sign(key, claims, kid: "override-kid")

sign!(key, claims, opts \\ [])

@spec sign!(NoWayJose.Key.t(), claims(), keyword()) :: token() | no_return()

Same as sign/3, but raises on error.

start_jwks_fetcher(name, url, opts \\ [])

@spec start_jwks_fetcher(String.t(), String.t(), keyword()) :: :ok | {:error, term()}

Starts a JWKS fetcher for an external endpoint.

Options

  • :refresh_interval - Refresh period in ms (default: 15 minutes)
  • :retry_interval - Retry on failure in ms (default: 30 seconds)
  • :sync_init - Block until first fetch completes (default: false)
  • :http_client - Custom HTTP client module
  • :http_opts - Options passed to the HTTP client

Examples

# Async start (returns immediately)
:ok = NoWayJose.start_jwks_fetcher("auth0",
  "https://example.auth0.com/.well-known/jwks.json"
)

# Sync start (blocks until keys are loaded)
:ok = NoWayJose.start_jwks_fetcher("google",
  "https://www.googleapis.com/oauth2/v3/certs",
  sync_init: true
)

stop_jwks_fetcher(name)

@spec stop_jwks_fetcher(String.t()) :: :ok | {:error, :not_found}

Stops a JWKS fetcher.

Examples

:ok = NoWayJose.stop_jwks_fetcher("auth0")

verify(key, token, opts \\ [])

@spec verify(NoWayJose.Key.t(), token(), validation_options()) ::
  {:ok, claims()} | {:error, atom()}

Verifies a token with a key.

Options

  • :validate_exp - Validate expiration claim (default: true)
  • :validate_nbf - Validate not-before claim (default: true)
  • :leeway - Clock skew tolerance in seconds (default: 0)
  • :iss - Required issuer(s) - string or list of strings
  • :aud - Required audience(s) - string or list of strings
  • :sub - Required subject
  • :required_claims - List of claim names that must be present

Examples

{:ok, claims} = NoWayJose.verify(key, token)

# With validation options
{:ok, claims} = NoWayJose.verify(key, token,
  aud: "my-app",
  iss: "https://auth.example.com",
  leeway: 60
)

Errors

Returns {:error, reason} where reason is one of:

  • :invalid_token - Malformed JWT
  • :invalid_signature - Signature verification failed
  • :expired_signature - Token has expired
  • :immature_signature - Token not yet valid (nbf)
  • :invalid_issuer - Issuer doesn't match
  • :invalid_audience - Audience doesn't match
  • :invalid_subject - Subject doesn't match
  • :missing_required_claim - Required claim not present

verify!(key, token, opts \\ [])

@spec verify!(NoWayJose.Key.t(), token(), validation_options()) ::
  claims() | no_return()

Same as verify/3, but raises on error.

verify_with_stored(token, name, opts \\ [])

@spec verify_with_stored(token(), String.t(), validation_options()) ::
  {:ok, claims()} | {:error, atom()}

Verifies a token using stored keys.

Automatically extracts the kid from the token header and looks up the matching key from the key store.

Examples

{:ok, claims} = NoWayJose.verify_with_stored(token, "auth0", aud: "my-app")

verify_with_stored!(token, name, opts \\ [])

@spec verify_with_stored!(token(), String.t(), validation_options()) ::
  claims() | no_return()

Same as verify_with_stored/3, but raises on error.