Skip to content

Authentication

Stoma has two auth-related groups:

  • Inbound auth/authorization policies that validate client requests before upstream dispatch.
  • Outbound signing/token policies that prepare requests for upstream services.
PolicyPriorityPurpose
jwtAuth10 (Priority.AUTH)Verify JWT bearer tokens (HMAC or JWKS RSA).
apiKeyAuth10Validate API keys from header/query.
basicAuth10Validate HTTP Basic credentials.
oauth210Validate OAuth2 access tokens via introspection or local callback.
rbac10Enforce role/permission checks from forwarded headers.
jws10Verify JWS compact signatures (embedded or detached payload).
verifyHttpSignature10Verify RFC 9421 HTTP Message Signatures.
generateJwt50 (Priority.REQUEST_TRANSFORM)Mint JWT and attach to upstream request headers.
generateHttpSignature95 (Priority.PROXY)Sign outbound requests with RFC 9421 headers.

Validate JWT tokens from a request header. Supports HMAC (HS256/384/512) and RSA via JWKS (RS256/384/512).

import { jwtAuth } from "@homegrower-club/stoma";
interface JwtAuthConfig {
secret?: string;
jwksUrl?: string;
issuer?: string;
audience?: string;
headerName?: string; // default: "authorization"
tokenPrefix?: string; // default: "Bearer"
forwardClaims?: Record<string, string>;
jwksCacheTtlMs?: number; // default: 300000
skip?: (c: Context) => boolean | Promise<boolean>;
}
  • Rejects alg: "none" (case-insensitive).
  • Validates exp when present, plus iss and aud when configured.
  • Sanitizes forwarded claims to remove \r, \n, and \0.
  • Caches JWKS responses in-memory per URL.
ConditionStatusError code
Missing token/header401unauthorized
Invalid token format/signature/claims401unauthorized
JWKS fetch failure502jwks_error

Validate API keys from a header, with optional query fallback.

import { apiKeyAuth } from "@homegrower-club/stoma";
interface ApiKeyAuthConfig {
headerName?: string; // default: "x-api-key"
queryParam?: string; // optional fallback
validate: (key: string) => boolean | Promise<boolean>;
skip?: (c: Context) => boolean | Promise<boolean>;
}
ConditionStatusError code
Missing API key401unauthorized
Validator returned false403forbidden

HTTP Basic authentication with browser challenge support (WWW-Authenticate).

import { basicAuth } from "@homegrower-club/stoma";
interface BasicAuthConfig {
validate: (username: string, password: string, c: Context) => boolean | Promise<boolean>;
realm?: string; // default: "Restricted"
skip?: (c: Context) => boolean | Promise<boolean>;
}
ConditionStatusError code
Missing/invalid Basic header401unauthorized
Invalid credentials403forbidden

Validate OAuth2 access tokens using either:

  • localValidate(token) callback, or
  • RFC 7662 introspection endpoint.

If both are provided, localValidate takes precedence.

import { oauth2 } from "@homegrower-club/stoma";
interface OAuth2Config {
introspectionUrl?: string;
clientId?: string;
clientSecret?: string;
localValidate?: (token: string) => boolean | Promise<boolean>;
tokenLocation?: "header" | "query"; // default: "header"
headerName?: string; // default: "authorization"
headerPrefix?: string; // default: "Bearer"
queryParam?: string; // default: "access_token"
forwardTokenInfo?: Record<string, string>;
cacheTtlSeconds?: number; // default: 0 (disabled)
requiredScopes?: string[];
skip?: (c: Context) => boolean | Promise<boolean>;
}
ConditionStatusError code
Missing token401unauthorized
Inactive/invalid token401unauthorized
Missing required scopes403forbidden
Introspection upstream error502introspection_error
Misconfiguration (no validation strategy)500config_error

Role/permission checks from request headers, usually after jwtAuth claim forwarding or oauth2 token info forwarding.

import { rbac } from "@homegrower-club/stoma";
interface RbacConfig {
roleHeader?: string; // default: "x-user-role"
roles?: string[]; // pass if user has ANY
permissionHeader?: string; // default: "x-user-permissions"
permissions?: string[]; // pass if user has ALL
roleDelimiter?: string; // default: ","
permissionDelimiter?: string; // default: ","
denyMessage?: string; // default: "Access denied: insufficient permissions"
skip?: (c: Context) => boolean | Promise<boolean>;
}

When both roles and permissions are set, both checks must pass.


Verify JWS compact serialization (header.payload.signature) using HMAC secret or JWKS RSA keys.

import { jws } from "@homegrower-club/stoma";
interface JwsConfig {
secret?: string;
jwksUrl?: string;
headerName?: string; // default: "X-JWS-Signature"
payloadSource?: "embedded" | "body"; // default: "embedded"
forwardPayload?: boolean; // default: false
forwardHeaderName?: string; // default: "X-JWS-Payload"
jwksCacheTtlMs?: number; // default: 300000
skip?: (c: Context) => boolean | Promise<boolean>;
}
  • payloadSource: "body" supports detached payload signatures.
  • Supported algorithms: HS256/384/512, RS256/384/512.
ConditionStatusError code
Missing signature header401jws_missing
Invalid format, alg, key, or signature401jws_invalid
JWKS fetch failure502jwks_error
Misconfiguration500config_error

Verify inbound RFC 9421 HTTP Message Signatures from Signature + Signature-Input headers.

import { verifyHttpSignature } from "@homegrower-club/stoma";
interface VerifyHttpSignatureConfig {
keys: Record<string, {
secret?: string;
publicKey?: JsonWebKey;
algorithm: string; // e.g. "hmac-sha256", "rsa-v1_5-sha256", "rsa-pss-sha512"
}>;
requiredComponents?: string[]; // default: ["@method"]
maxAge?: number; // default: 300 seconds
signatureHeaderName?: string; // default: "Signature"
signatureInputHeaderName?: string; // default: "Signature-Input"
label?: string; // default: "sig1"
skip?: (c: Context) => boolean | Promise<boolean>;
}

All verification failures return 401 with signature_invalid. Misconfiguration (keys missing/empty) returns 500 with config_error.


Mint a JWT during request processing and attach it to a request header before upstream dispatch.

import { generateJwt } from "@homegrower-club/stoma";
interface GenerateJwtConfig {
algorithm: "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512";
secret?: string;
privateKey?: JsonWebKey;
claims?: Record<string, unknown> | ((c: Context) => Record<string, unknown> | Promise<Record<string, unknown>>);
expiresIn?: number; // default: 3600
issuer?: string;
audience?: string;
headerName?: string; // default: "Authorization"
tokenPrefix?: string; // default: "Bearer"
skip?: (c: Context) => boolean | Promise<boolean>;
}
  • Missing required key material for the chosen algorithm returns 500 config_error.

Sign outbound requests with RFC 9421 headers (Signature-Input, Signature).

import { generateHttpSignature } from "@homegrower-club/stoma";
interface GenerateHttpSignatureConfig {
keyId: string;
algorithm: string; // "hmac-sha256", "rsa-v1_5-sha256", "rsa-pss-sha512"
secret?: string;
privateKey?: JsonWebKey;
components?: string[]; // default: ["@method", "@path", "@authority"]
signatureHeaderName?: string; // default: "Signature"
signatureInputHeaderName?: string; // default: "Signature-Input"
label?: string; // default: "sig1"
expires?: number; // seconds from created timestamp
nonce?: boolean; // default: false
skip?: (c: Context) => boolean | Promise<boolean>;
}
  • Missing signing material returns 500 config_error.
  • Unsupported algorithm returns a runtime error from crypto helper validation.

import { createGateway, jwtAuth, rbac } from "@homegrower-club/stoma";
createGateway({
routes: [{
path: "/admin/*",
pipeline: {
policies: [
jwtAuth({
jwksUrl: env.JWKS_URL,
forwardClaims: { roles: "x-user-role", perms: "x-user-permissions" },
}),
rbac({
roles: ["admin"],
permissions: ["users:write"],
}),
],
upstream: { type: "url", target: "https://admin.internal" },
},
}],
});

You can clear module-level verification caches in tests:

import { clearJwksCache, clearOAuth2Cache, clearJwsJwksCache } from "@homegrower-club/stoma";
clearJwksCache();
clearOAuth2Cache();
clearJwsJwksCache();