diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a5c8be2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# Normalize line endings to LF on commit +* text=auto eol=lf + +# Explicitly declare text files +*.ts text eol=lf +*.js text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +*.py text eol=lf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary diff --git a/ENHANCEMENT_JWKS_HTTP.md b/ENHANCEMENT_JWKS_HTTP.md new file mode 100644 index 0000000..a4218a5 --- /dev/null +++ b/ENHANCEMENT_JWKS_HTTP.md @@ -0,0 +1,563 @@ +# Enhancement Request: Add JWT_JWKS_URL Support for HTTP JWKS Endpoints + +**Status**: Enhancement Request +**Priority**: High +**Target Package**: `@chrislyons-dev/flarelette-jwt` +**Impact**: Enables gateway workers to verify external OIDC tokens from Auth0, Okta, Google, Azure AD, and Cloudflare Access + +--- + +## Executive Summary + +Add support for `JWT_JWKS_URL` environment variable to enable HTTP-based JWKS fetching. This allows gateway workers to verify external OIDC tokens from standard identity providers while maintaining the existing service binding pattern for internal service mesh. + +**Current State**: Only supports service bindings (`JWT_JWKS_SERVICE_NAME`) and inline keys (`JWT_PUBLIC_JWK_NAME`) +**Desired State**: Add HTTP JWKS URL support (`JWT_JWKS_URL`) with caching + +--- + +## Motivation + +### Use Case: Gateway + Service Mesh Architecture + +**Typical flarelette deployment pattern:** + +1. **Gateway Worker**: Verifies external OIDC tokens (Auth0, Okta, etc.) → needs HTTP JWKS +2. **Internal Services**: Verify internal tokens from gateway → uses service bindings (existing) + +Both gateway and internal services use `flarelette-hono` middleware. The only difference is JWKS resolution strategy: + +| Component | JWKS Strategy | Current Support | +| ----------------- | ----------------------------------------- | --------------- | +| Gateway | HTTP JWKS URL (`JWT_JWKS_URL`) | ❌ **Missing** | +| Internal Services | Service Binding (`JWT_JWKS_SERVICE_NAME`) | ✅ Exists | +| Internal Services | Inline Public Key (`JWT_PUBLIC_JWK_NAME`) | ✅ Exists | + +**Problem**: Without `JWT_JWKS_URL`, gateway workers cannot verify external OIDC tokens using standard OIDC discovery patterns. + +**Current Workaround**: None - users must manually fetch JWKS and convert to inline JWK, which breaks on key rotation. + +--- + +## Requirements + +### 1. Environment Variable + +Add support for `JWT_JWKS_URL` environment variable: + +```bash +# Gateway configuration +JWT_JWKS_URL=https://auth0.example.com/.well-known/jwks.json +JWT_ISS=https://auth0.example.com/ +JWT_AUD=my-app-client-id +JWT_LEEWAY_SECONDS=300 +``` + +**Type**: Public URL (not a secret) - should be in `[vars]` section of wrangler.toml, not secrets. + +### 2. JWKS Resolution Priority + +Update JWKS resolution strategy to include HTTP URLs: + +**Current Priority**: + +1. Service Binding (`JWT_JWKS_SERVICE_NAME`) +2. Inline Public Key (`JWT_PUBLIC_JWK_NAME`) + +**New Priority**: + +1. Service Binding (`JWT_JWKS_SERVICE_NAME`) - preferred for internal mesh +2. Inline Public Key (`JWT_PUBLIC_JWK_NAME`) - for simple deployments +3. **HTTP JWKS URL (`JWT_JWKS_URL`)** - NEW - for gateway/external OIDC + +### 3. HTTP JWKS Fetching + +**Fetch Behavior**: + +- Use `fetch()` to retrieve JWKS from `JWT_JWKS_URL` +- Parse JSON response as RFC 7517 JWKS structure +- Extract keys array and validate structure + +**Example JWKS Response**: + +```json +{ + "keys": [ + { + "kid": "key-2025-01", + "kty": "RSA", + "use": "sig", + "alg": "RS256", + "n": "...", + "e": "AQAB" + }, + { + "kid": "key-2024-12", + "kty": "RSA", + "use": "sig", + "alg": "RS256", + "n": "...", + "e": "AQAB" + } + ] +} +``` + +**Key Matching**: + +- Match JWT header `kid` to JWKS `keys[].kid` +- Return matching key for verification +- Error if `kid` not found in JWKS + +### 4. Caching Strategy + +**Requirements**: + +- In-memory cache per Worker instance +- 5-minute cooldown between JWKS fetches (configurable) +- Automatic cache invalidation on key-not-found +- Thread-safe cache access + +**Cache Key**: `JWT_JWKS_URL` value + +**Cache Invalidation**: + +- Time-based: 5 minutes (default) +- Error-based: Immediate retry on 404/network error +- Key-not-found: Fetch fresh JWKS if requested `kid` not in cache + +**Example Cache Logic**: + +```typescript +interface JWKSCache { + keys: JsonWebKey[] + fetchedAt: number + url: string +} + +const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes + +async function getJWKS(url: string): Promise { + const cached = jwksCache.get(url) + const now = Date.now() + + if (cached && now - cached.fetchedAt < CACHE_TTL_MS) { + return cached.keys + } + + const response = await fetch(url) + if (!response.ok) { + throw new Error(`JWKS fetch failed: ${response.status}`) + } + + const jwks = await response.json() + + jwksCache.set(url, { + keys: jwks.keys, + fetchedAt: now, + url, + }) + + return jwks.keys +} +``` + +### 5. Error Handling + +**Configuration Errors** (fail fast): + +- Invalid URL format → `Error: Invalid JWT_JWKS_URL format` +- HTTPS required → `Error: JWT_JWKS_URL must use HTTPS` +- Both `JWT_JWKS_URL` and `JWT_JWKS_SERVICE_NAME` set → `Error: Cannot use both JWT_JWKS_URL and JWT_JWKS_SERVICE_NAME` + +**Runtime Errors** (fail silent for verification): + +- HTTP 404/500 → Return `null` (verification fails) +- Network timeout → Return `null` +- Invalid JSON → Return `null` +- Key not found → Return `null` +- Malformed JWKS → Return `null` + +**Security Note**: Never leak JWKS URL or key details in error messages. Return generic "Invalid or expired token" for all verification failures. + +### 6. Security Considerations + +**HTTPS Only**: + +- Reject `http://` URLs +- Only accept `https://` for JWKS fetching +- Exception: Allow `http://localhost` and `http://127.0.0.1` for testing + +**URL Validation**: + +- Validate URL format before fetching +- Prevent SSRF attacks (reject internal IPs in production) +- Set reasonable timeout (5 seconds recommended) + +**Cache Security**: + +- Cache only public keys (never private keys) +- Clear cache on Worker restart +- No persistent storage of JWKS + +### 7. Testing Requirements + +**Unit Tests**: + +- ✅ Fetch JWKS from valid URL +- ✅ Parse standard JWKS response +- ✅ Match `kid` to key in JWKS +- ✅ Return `null` if `kid` not found +- ✅ Cache JWKS for 5 minutes +- ✅ Refresh cache after TTL expires +- ✅ Handle HTTP 404 gracefully +- ✅ Handle network timeout +- ✅ Handle malformed JSON +- ✅ Reject `http://` URLs (non-HTTPS) +- ✅ Allow `http://localhost` for testing +- ✅ Verify EdDSA tokens from HTTP JWKS +- ✅ Verify RSA tokens from HTTP JWKS + +**Integration Tests**: + +- ✅ Verify Auth0 token with live JWKS URL +- ✅ Verify Okta token with live JWKS URL +- ✅ Verify Cloudflare Access token with live JWKS URL +- ✅ Handle key rotation (dual-key JWKS) +- ✅ Cache invalidation on key-not-found + +**Performance Tests**: + +- First fetch: ~50-100ms (HTTP request) +- Cached fetch: <1ms (in-memory) +- Cache hit rate: >95% in steady state + +### 8. Configuration Examples + +#### Auth0 + +```toml +# wrangler.toml +[vars] +JWT_ISS = "https://your-tenant.auth0.com/" +JWT_AUD = "your-client-id" +JWT_JWKS_URL = "https://your-tenant.auth0.com/.well-known/jwks.json" +JWT_LEEWAY_SECONDS = "300" +``` + +**Notes**: + +- Issuer **must** include trailing slash +- JWKS URL follows standard OIDC discovery + +#### Okta + +```toml +# wrangler.toml +[vars] +JWT_ISS = "https://your-domain.okta.com/oauth2/default" +JWT_AUD = "api://default" +JWT_JWKS_URL = "https://your-domain.okta.com/oauth2/default/v1/keys" +JWT_LEEWAY_SECONDS = "300" +``` + +**Notes**: + +- Authorization server ID in path (`default` or custom) +- JWKS URL uses `/v1/keys` endpoint + +#### Google Workspace + +```toml +# wrangler.toml +[vars] +JWT_ISS = "https://accounts.google.com" +JWT_AUD = "123456789-abcdefg.apps.googleusercontent.com" +JWT_JWKS_URL = "https://www.googleapis.com/oauth2/v3/certs" +JWT_LEEWAY_SECONDS = "300" +``` + +**Notes**: + +- Audience is OAuth 2.0 client ID +- JWKS URL is Google's public certs endpoint + +#### Azure AD (Microsoft Entra ID) + +```toml +# wrangler.toml +[vars] +JWT_ISS = "https://login.microsoftonline.com/your-tenant-id/v2.0" +JWT_AUD = "api://your-app-client-id" +JWT_JWKS_URL = "https://login.microsoftonline.com/your-tenant-id/discovery/v2.0/keys" +JWT_LEEWAY_SECONDS = "300" +``` + +**Notes**: + +- Replace `your-tenant-id` with actual tenant GUID +- JWKS URL is tenant-specific + +#### Cloudflare Access + +```toml +# wrangler.toml +[vars] +JWT_ISS = "https://your-team.cloudflareaccess.com" +JWT_AUD = "abc123def456ghi789" +JWT_JWKS_URL = "https://your-team.cloudflareaccess.com/cdn-cgi/access/certs" +JWT_LEEWAY_SECONDS = "300" +``` + +**Notes**: + +- Non-standard JWKS path (`/cdn-cgi/access/certs`) +- AUD is application AUD tag from Access policy +- Standard RFC 7517 JWKS format (despite non-standard path) + +--- + +## Implementation Guidance + +### Suggested Code Location + +**Package**: `@chrislyons-dev/flarelette-jwt` +**File**: `src/jwks.ts` (or existing JWKS resolution module) + +### Key Changes Required + +1. **Add `JWT_JWKS_URL` to `WorkerEnv` interface** (src/types.ts): + +```typescript +export interface WorkerEnv extends Record { + // ... existing vars + JWT_JWKS_URL?: string // NEW +} +``` + +2. **Update JWKS resolution logic** (src/jwks.ts or similar): + +```typescript +async function resolveJWKS(env: WorkerEnv): Promise { + // Priority 1: Service binding + if (env.JWT_JWKS_SERVICE_NAME && env[env.JWT_JWKS_SERVICE_NAME]) { + return fetchJWKSViaBinding(env[env.JWT_JWKS_SERVICE_NAME]) + } + + // Priority 2: Inline public key + if (env.JWT_PUBLIC_JWK_NAME && env[env.JWT_PUBLIC_JWK_NAME]) { + return [parseInlineJWK(env[env.JWT_PUBLIC_JWK_NAME])] + } + + // Priority 3: HTTP JWKS URL (NEW) + if (env.JWT_JWKS_URL) { + return fetchJWKSViaHTTP(env.JWT_JWKS_URL) + } + + throw new Error('No JWKS source configured') +} + +async function fetchJWKSViaHTTP(url: string): Promise { + validateJWKSURL(url) + + const cached = getJWKSFromCache(url) + if (cached) return cached + + const response = await fetch(url, { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(5000), // 5 second timeout + }) + + if (!response.ok) { + throw new Error(`JWKS fetch failed: ${response.status}`) + } + + const jwks = (await response.json()) as JWKSResponse + + if (!jwks.keys || !Array.isArray(jwks.keys)) { + throw new Error('Invalid JWKS response') + } + + cacheJWKS(url, jwks.keys) + + return jwks.keys +} + +function validateJWKSURL(url: string): void { + try { + const parsed = new URL(url) + + // HTTPS required (except localhost for testing) + if (parsed.protocol !== 'https:') { + const isLocalhost = + parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' + if (!isLocalhost || parsed.protocol !== 'http:') { + throw new Error('JWT_JWKS_URL must use HTTPS') + } + } + } catch (error) { + throw new Error('Invalid JWT_JWKS_URL format') + } +} +``` + +3. **Add caching layer**: + +```typescript +interface CacheEntry { + keys: JsonWebKey[] + fetchedAt: number +} + +const jwksCache = new Map() +const CACHE_TTL_MS = 5 * 60 * 1000 + +function getJWKSFromCache(url: string): JsonWebKey[] | null { + const entry = jwksCache.get(url) + if (!entry) return null + + const age = Date.now() - entry.fetchedAt + if (age > CACHE_TTL_MS) { + jwksCache.delete(url) + return null + } + + return entry.keys +} + +function cacheJWKS(url: string, keys: JsonWebKey[]): void { + jwksCache.set(url, { + keys, + fetchedAt: Date.now(), + }) +} +``` + +### Backward Compatibility + +**Guaranteed**: Existing configurations continue to work without changes. + +- ✅ `JWT_JWKS_SERVICE_NAME` still takes priority +- ✅ `JWT_PUBLIC_JWK_NAME` still works +- ✅ No breaking changes to API +- ✅ New feature is opt-in via `JWT_JWKS_URL` + +### Performance Impact + +**Gateway Workers** (new HTTP JWKS): + +- First request: +50-100ms (HTTP JWKS fetch) +- Subsequent requests: +<1ms (cache hit) +- Cache expires: +50-100ms every 5 minutes + +**Internal Services** (existing service bindings): + +- No change (service bindings still preferred) + +--- + +## Documentation Updates Required + +### 1. README.md + +- Add `JWT_JWKS_URL` to environment variables table +- Add gateway configuration example +- Update JWKS resolution priority + +### 2. Configuration Guide + +- Add HTTP JWKS strategy +- Document caching behavior +- Add OIDC provider examples (Auth0, Okta, Google, Azure AD, Cloudflare Access) + +### 3. API Documentation + +- Document `JWT_JWKS_URL` variable +- Security considerations (HTTPS-only) +- Performance characteristics (caching) + +--- + +## Success Criteria + +**Implementation Complete When**: + +1. ✅ `JWT_JWKS_URL` environment variable supported +2. ✅ HTTP JWKS fetching works for all standard OIDC providers +3. ✅ 5-minute caching implemented and tested +4. ✅ HTTPS validation enforces security +5. ✅ All unit tests pass (95%+ coverage) +6. ✅ Integration tests with Auth0, Okta, Cloudflare Access pass +7. ✅ Documentation updated (README, config guide, API docs) +8. ✅ Backward compatibility verified (existing configs unchanged) + +**Validation Tests**: + +```bash +# Test 1: Verify Auth0 token +JWT_JWKS_URL=https://your-tenant.auth0.com/.well-known/jwks.json +JWT_ISS=https://your-tenant.auth0.com/ +JWT_AUD=your-client-id + +# Test 2: Verify Cloudflare Access token +JWT_JWKS_URL=https://your-team.cloudflareaccess.com/cdn-cgi/access/certs +JWT_ISS=https://your-team.cloudflareaccess.com +JWT_AUD=your-aud-tag + +# Test 3: Cache performance +# First request: ~50-100ms +# Second request (cached): <1ms +# After 5 minutes: ~50-100ms (cache refresh) +``` + +--- + +## Questions for Implementation + +1. **Cache TTL Configuration**: Should `JWT_JWKS_CACHE_TTL_SECONDS` be configurable, or fixed at 5 minutes? + - Recommendation: Fixed at 5 minutes initially, add env var if needed + +2. **Algorithm Support**: Should we support RSA (RS256, RS384, RS512) in addition to EdDSA? + - Recommendation: Yes - most OIDC providers use RSA + +3. **JWKS Size Limit**: Should we limit JWKS response size (e.g., 100KB max)? + - Recommendation: Yes - prevent DOS via large JWKS responses + +4. **Retry Logic**: Should we retry failed JWKS fetches? + - Recommendation: No - fail fast and return `null` for verification + +5. **Metrics**: Should we expose JWKS cache hit/miss metrics? + - Recommendation: Not in v1 - add later if needed + +--- + +## References + +### Standards + +- [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517) +- [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) +- [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) + +### OIDC Provider JWKS URLs + +- **Auth0**: `https://{tenant}.auth0.com/.well-known/jwks.json` +- **Okta**: `https://{domain}.okta.com/oauth2/{authServerId}/v1/keys` +- **Google**: `https://www.googleapis.com/oauth2/v3/certs` +- **Azure AD**: `https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys` +- **Cloudflare Access**: `https://{team}.cloudflareaccess.com/cdn-cgi/access/certs` + +### Related Documentation + +- [Cloudflare Workers fetch() API](https://developers.cloudflare.com/workers/runtime-apis/fetch/) +- [Web Crypto API](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) + +--- + +## Contact + +**Package**: `@chrislyons-dev/flarelette-jwt` +**Repository**: `C:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-ts\` +**Requesting Package**: `@chrislyons-dev/flarelette-hono` +**Date**: 2025-12-07 diff --git a/ENHANCEMENT_JWKS_HTTP_feedback.md b/ENHANCEMENT_JWKS_HTTP_feedback.md new file mode 100644 index 0000000..ebdecaf --- /dev/null +++ b/ENHANCEMENT_JWKS_HTTP_feedback.md @@ -0,0 +1,110 @@ +Yeah, this is in a _really_ good place. You’ve clearly thought through the classic JWT foot-guns and closed most of the obvious doors. + +I’ll call out where I think you’re solid, and then a few small polish / hardening ideas. + +--- + +## Overall: are the vulns mitigated? + +**Yes** – based on what you wrote, the big historical issues are covered: + +- **Alg `none` & algo confusion:** strict whitelists + `jose` + separate HS vs asym paths. +- **RS→HS confusion:** symmetric vs asymmetric keys are **never shared**; mode is driven by config, not by `alg`; you explicitly error if both HS and asym are configured. +- **JWKS abuse:** JWKS URL is pinned in config, tokens cannot override with `jku`/`x5u`. +- **`kid` abuse:** treated as a pure lookup key, not interpolated into SQL/paths/URLs. +- **HS512 secret strength:** 64-byte minimum with explicit enforcement. +- **Single-mode requirement:** config error if HS + asym are both set. + +From a design perspective, this is _much_ safer than the majority of “roll your own JWT” setups in the wild. + +--- + +## A few concrete suggestions + +### 1. Be consistent about which algorithms are actually supported + +At the top you list: + +- HS512 +- EdDSA (Ed25519) + +But later you say: + +> EdDSA/RSA mode: `algorithms: ['EdDSA', 'RS256', 'RS384', 'RS512']` only + +If the library **actually supports RSA** in this mode, I’d reflect that in the top table: + +```md +| Profile | Algorithm | Key Type | Use Case | +| ------- | ------------- | -------------- | ------------------------------- | +| HS512 | HMAC-SHA-512 | 64-byte secret | Trusted producer-consumer pairs | +| EdDSA | Ed25519 | JWK/JWKS | Public verification | +| RSA | RS256/384/512 | JWK/JWKS | Public verification (optional) | +``` + +If you **don’t** really intend to support RSA right now, I’d _drop_ the RS algorithms from the whitelist and just say: + +> EdDSA mode: `algorithms: ['EdDSA']` only + +Narrower is always safer. + +--- + +### 2. Make the “mode determined by config” super explicit + +You describe it correctly, but I’d tighten the language so nobody later “simplifies” it back to trusting `alg`: + +> **Mode selection** +> +> - Verification mode (HS512 vs EdDSA/RSA) is chosen **only from server configuration**, never from the token header. +> - The `alg` header is treated as _untrusted input_ and must match the allowed algorithms for the selected mode. Mismatches are rejected. + +That matches what you’re already doing in code, and it makes the design intent obvious for future maintainers. + +--- + +### 3. JWKS: mention per-key `alg` pinning (which you’re basically already doing) + +You already mention: + +> Inline JWK imports explicitly specify expected algorithm: `importJWK(jwk, 'EdDSA')` + +That’s great. I’d add one short line: + +> When importing JWKs, the expected algorithm (`'EdDSA'`, `'RS256'`, etc.) is provided explicitly, so keys cannot be repurposed for other algorithms even within the same key family. + +That signals that you’re not just whitelisting “any RS\*”, you’re actually pinning at import time too. + +--- + +### 4. Fail-silent: add _logging_ to keep observability + +Returning `null` to callers is fine, but I’d explicitly say: + +> Internally, verification failures are **logged with structured metadata** (issuer, kid, reason category) and counted in metrics. Externally, all failures are returned as `null` to avoid leaking details. + +Otherwise someone might over-interpret “fail-silent” as “we don’t log anything,” which would be painful in prod. + +--- + +### 5. A couple of small wording / clarity tweaks + +- You sometimes say **“decrypt key”** – for JWT as you’re using it, it’s **sign/verify**, not encrypt/decrypt. I’d keep wording to “signing key” / “verification key” to avoid confusion with JWE. +- In “Security Checklist,” maybe add: + - `[ ] JWT_AUD is specific per service (no wildcard audiences)` – avoids token reuse between services. + +--- + +## Bottom line + +From a security-model standpoint, this looks **strong and well-documented**: + +- No `alg:none` +- No RS↔HS confusion +- No header-controlled JWKS/JKU +- Strong HS512 key requirements +- Single-mode enforcement (HS _or_ asym, not both) +- EdDSA with JWKS + optional thumbprint pinning +- Reasonable claim validation (`iss`, `aud`, `exp`, `nbf`, `iat`) + +If you clean up the minor consistency bits (RSA vs not, “decrypt” wording, explicit mention of logging), I’d feel very comfortable shipping this as the public “here’s why you can trust our JWT handling” story for flarelette. diff --git a/README.md b/README.md index fc4ff6c..7db6b4f 100644 --- a/README.md +++ b/README.md @@ -123,12 +123,33 @@ if payload: ## Key Features -- **Algorithm auto-detection** — Chooses HS512 or EdDSA based on environment variables +- **Algorithm auto-detection** — Chooses HS512, EdDSA, or RSA based on environment variables +- **HTTP JWKS for OIDC** — Verify tokens from Auth0, Okta, Google, Azure AD, and Cloudflare Access (TypeScript) - **Secret-name indirection** — References Cloudflare secret bindings instead of raw values - **Identical TypeScript + Python APIs** — Same function names and behavior across languages - **Service bindings for JWKS** — Direct Worker-to-Worker RPC for key distribution - **Zero-trust delegation** — RFC 8693 actor claims for service-to-service authentication - **Policy-based authorization** — Fluent API for composing permission and role requirements +- **Explicit configuration API** — Test without environment variables using config objects + +## Security + +Flarelette JWT Kit is designed to prevent common JWT vulnerabilities: + +- **No algorithm confusion** — Mode determined by server configuration only, never from token headers. Strict algorithm whitelists per mode. +- **No RS↔HS attacks** — Symmetric and asymmetric keys never shared. Configuration error thrown if both HS512 and EdDSA/RSA secrets configured. +- **No JWKS injection** — JWKS URLs pinned in configuration, `jku`/`x5u` headers ignored. +- **Strong secrets enforced** — 64-byte minimum for HS512 (512 bits), matching SHA-512 digest size. +- **Algorithm pinning at import** — Keys imported with explicit algorithm specification, preventing repurposing. + +**Mode selection is driven exclusively by server environment variables:** + +- HS512 mode: `algorithms: ['HS512']` only +- EdDSA/RSA mode: `algorithms: ['EdDSA', 'RS256', 'RS384', 'RS512']` only + +The `alg` header is treated as untrusted input and must match the allowed algorithms for the selected mode. Mismatches are rejected. + +**For complete security documentation**, see [docs/security-guide.md](./docs/security-guide.md). ## Configuration @@ -162,6 +183,59 @@ JWT_PUBLIC_JWK_NAME=GATEWAY_PUBLIC_KEY JWT_JWKS_SERVICE_NAME=GATEWAY_BINDING ``` +**HTTP JWKS mode** (external OIDC providers - TypeScript only): + +Verify tokens from Auth0, Okta, Google, Azure AD, and other OIDC providers: + +```bash +# Environment-based configuration: +JWT_ISS=https://tenant.auth0.com/ +JWT_AUD=your-client-id +JWT_JWKS_URL=https://tenant.auth0.com/.well-known/jwks.json +JWT_JWKS_CACHE_TTL_SECONDS=300 # Optional: cache duration (default: 5 minutes) +``` + +**Explicit configuration (no environment setup):** + +```typescript +import { + verifyWithConfig, + createJWKSUrlVerifyConfig, +} from '@chrislyons-dev/flarelette-jwt' + +const config = createJWKSUrlVerifyConfig( + 'https://tenant.auth0.com/.well-known/jwks.json', + { + iss: 'https://tenant.auth0.com/', + aud: 'your-client-id', + }, + 300 // cacheTtl in seconds +) + +const payload = await verifyWithConfig(token, config) +``` + +**Supported OIDC providers:** + +- **Auth0:** `https://tenant.auth0.com/.well-known/jwks.json` +- **Okta:** `https://domain.okta.com/oauth2/default/v1/keys` +- **Google:** `https://www.googleapis.com/oauth2/v3/certs` +- **Azure AD:** `https://login.microsoftonline.com/tenant-id/discovery/v2.0/keys` +- **Cloudflare Access:** `https://team.cloudflareaccess.com/cdn-cgi/access/certs` + +> **Note:** HTTP JWKS is TypeScript-only. Python support pending Cloudflare runtime improvements. + +### Verification Key Resolution Priority + +When verifying tokens, the library uses the first available key source in this order: + +1. **HS512 shared secret** — `JWT_SECRET` or `JWT_SECRET_NAME` +2. **Inline public JWK** — `JWT_PUBLIC_JWK` or `JWT_PUBLIC_JWK_NAME` +3. **Service binding JWKS** — `JWT_JWKS_SERVICE` or `JWT_JWKS_SERVICE_NAME` (TypeScript only) +4. **HTTP JWKS URL** — `JWT_JWKS_URL` (TypeScript only) + +**Security note:** The library prevents mode confusion by rejecting configurations that mix symmetric (HS512) and asymmetric (EdDSA/RSA) secrets. + ## Documentation - **[Getting Started](./docs/getting-started.md)** — Installation, first token, and basic setup diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index c1f35d0..9b0d9c0 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -17,7 +17,7 @@ The TypeScript package depends on the following NPM packages: @flarelette/jwt-kit-env@1.8.1 │ C:\Users\chris\git\flarelette-jwt-kit │ -└─┬ @chrislyons-dev/flarelette-jwt@1.11.0 -> .\packages\flarelette-jwt-ts +└─┬ @chrislyons-dev/flarelette-jwt@1.12.0 -> .\packages\flarelette-jwt-ts │ Environment-driven JWT authentication for Cloudflare Workers with secret-name indirection └── jose@6.1.3 JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes @@ -77,4 +77,4 @@ This script: --- -**Last generated**: 2025-12-08 +**Last generated**: 2025-12-09 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 8b6d8fe..6400b1a 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,7 +1,7 @@ # 🏗️ flarelette-jwt-kit **Architecture Documentation** -Generated 2025-12-07 21:02:30 +Generated 2025-12-08 19:38:11 ## Overview diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt.md b/docs/architecture/chrislyons_dev_flarelette_jwt.md index e1fac41..4602856 100644 --- a/docs/architecture/chrislyons_dev_flarelette_jwt.md +++ b/docs/architecture/chrislyons_dev_flarelette_jwt.md @@ -83,10 +83,7 @@ environments or when working with multiple JWT configurations. util module -High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | JSON Web Key Set (JWKS) utilities. - -This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). -It supports integration with external JWKS services. | Key generation utility for EdDSA keys. +High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | Key generation utility for EdDSA keys. This script generates EdDSA key pairs and exports them in JWK format. It is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities. @@ -95,10 +92,7 @@ This module provides functions to generate secure secrets and validate base64url It ensures compatibility with JWT signing requirements. | Utility functions for JWT operations. This module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes. -It is designed to support core JWT functionalities. | JWT verification utilities. - -This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. -It supports integration with JWKS services and thumbprint pinning. +It is designed to support core JWT functionalities. View → @@ -111,6 +105,15 @@ It serves as the main interface for library consumers. View → +jwks +module +JSON Web Key Set (JWKS) utilities. + +This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). +It supports integration with external JWKS services. +View → + + types module Type definitions for JWT operations. @@ -120,6 +123,15 @@ It ensures type safety and consistency across the library. View → +verify +module +JWT verification utilities. + +This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. +It supports integration with JWKS services and thumbprint pinning. +View → + + adapters module Component inferred from directory: adapters diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__core.md b/docs/architecture/chrislyons_dev_flarelette_jwt__core.md index 1838eca..3ab0818 100644 --- a/docs/architecture/chrislyons_dev_flarelette_jwt__core.md +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__core.md @@ -47,7 +47,7 @@ It supports custom claims and configuration overrides. ### Code Elements
-9 code element(s) +11 code element(s) @@ -132,7 +132,7 @@ Returns partial JwtProfile-compatible configuration Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:54 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:65 @@ -161,7 +161,7 @@ Returns complete JwtProfile with detected algorithm Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:67 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:78 @@ -190,7 +190,7 @@ Returns complete JwtProfile with detected algorithm Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:82 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:93 @@ -217,7 +217,7 @@ Returns complete JwtProfile with detected algorithm Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:109 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:126 @@ -244,7 +244,7 @@ Returns complete JwtProfile with detected algorithm Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:115 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:132 @@ -271,7 +271,61 @@ Returns complete JwtProfile with detected algorithm Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:121 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:138 + + + + + + +--- +##### `getJwksUrl()` + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsstring
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:144
+ + + +--- +##### `getJwksCacheTtl()` + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsnumber
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts:148
diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md b/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md index 167c91e..4d5ea5c 100644 --- a/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md @@ -43,7 +43,7 @@ environments or when working with multiple JWT configurations. ### Code Elements
-8 code element(s) +9 code element(s) @@ -73,7 +73,7 @@ Sign a JWT token with explicit configuration Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:102 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:122 @@ -111,7 +111,7 @@ Verify a JWT token with explicit configuration Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:160 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:181 @@ -151,7 +151,7 @@ Higher-level wrapper around signWithConfig for convenience. Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:209 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:246 @@ -187,7 +187,7 @@ Implements RFC 8693 actor claim pattern for service-to-service delegation. Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:246 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:283 @@ -225,7 +225,7 @@ Verify and authorize a JWT token with explicit configuration Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:332 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:369 @@ -259,7 +259,7 @@ Helper function to create HS512 config from base64url-encoded secret Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:386 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:423 @@ -289,7 +289,7 @@ Helper function to create EdDSA sign config from JWK Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:414 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:452 @@ -319,7 +319,7 @@ Helper function to create EdDSA verify config from JWK Location -C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:436 +C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:474 @@ -328,6 +328,42 @@ Helper function to create EdDSA verify config from JWK - `publicJwk`: any — - Public JWK object or JSON string- `baseConfig`: Omit & Partial> — - Base JWT configuration +--- +##### `createJWKSUrlVerifyConfig()` + +Helper function to create HTTP JWKS URL verification config + +Enables testing without environment variables by providing explicit configuration + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsimport("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").JWKSUrlVerifyConfig — JWKS URL verification configuration
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:511
+ +**Parameters:** + +- `jwksUrl`: string — - HTTP(S) URL to JWKS endpoint- `baseConfig`: Omit & Partial> — - Base JWT configuration- `cacheTtl`: number — - Optional cache TTL in seconds (default: 300) +**Examples:** +```typescript + +``` + ---
diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__jwks.md b/docs/architecture/chrislyons_dev_flarelette_jwt__jwks.md new file mode 100644 index 0000000..de680ad --- /dev/null +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__jwks.md @@ -0,0 +1,284 @@ +# jwks — Code View + +[← Back to Container](./chrislyons_dev_flarelette_jwt.md) | [← Back to System](./README.md) + +--- + +## Component Information + + + + + + + + + + + + + + + + + + + + +
Componentjwks
Container@chrislyons-dev/flarelette-jwt
Typemodule
DescriptionJSON Web Key Set (JWKS) utilities. + +This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). +It supports integration with external JWKS services.
+ +--- + +## Code Structure + +### Class Diagram + +![Class Diagram](./diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.png) + +### Code Elements + +
+7 code element(s) + + + +#### Functions + +##### `clearJwksCache()` + +Clear the JWKS cache (for testing purposes) + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsvoid
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:49
+ + + +--- +##### `clearHttpJwksCache()` + +Clear the HTTP JWKS cache (for testing purposes) + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsvoid
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:57
+ + + +--- +##### `fetchJwksFromService()` + +Fetch JWKS from a service binding +Implements 5-minute caching to reduce load on JWKS service + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:65
+ +**Parameters:** + +- `service`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").Fetcher + +--- +##### `validateJwksUrl()` + +Validate JWKS URL for security requirements + +Requirements: +- Must be valid URL format +- Must use HTTPS (except localhost/127.0.0.1/[::1] for testing) + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilityprivate
ReturnsURL — Parsed URL object
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:103
+ +**Parameters:** + +- `url`: string — - JWKS URL to validate + +--- +##### `fetchJwksFromUrl()` + +Fetch JWKS from HTTP URL with caching + +Implements configurable TTL caching (default 5 minutes) +Security: HTTPS-only (except localhost), 5-second timeout, 100KB size limit + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Array of JWK objects
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:138
+ +**Parameters:** + +- `url`: string — - HTTP(S) URL to JWKS endpoint- `ttlSeconds`: number — - Cache TTL in seconds (default: 300) + +--- +##### `getKeyFromJwks()` + +Find and import a specific key from JWKS by kid + +Supports both EdDSA (Ed25519) and RSA (RS256/RS384/RS512) keys +Algorithm is auto-detected from key type (kty) and curve (crv) + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise | CryptoKey> — CryptoKey or Uint8Array suitable for jose verification
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:209
+ +**Parameters:** + +- `kid`: string — - Key ID from JWT header- `jwks`: JWKWithKid[] — - Array of JWK objects + +--- +##### `allowedThumbprints()` + +Get allowed thumbprints for key pinning (optional security measure) + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
ReturnsSet
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:242
+ + + +--- + +
+ +--- + + diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__util.md b/docs/architecture/chrislyons_dev_flarelette_jwt__util.md index 754cdbe..013cbb7 100644 --- a/docs/architecture/chrislyons_dev_flarelette_jwt__util.md +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__util.md @@ -22,10 +22,7 @@ Description -High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | JSON Web Key Set (JWKS) utilities. - -This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). -It supports integration with external JWKS services. | Key generation utility for EdDSA keys. +High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | Key generation utility for EdDSA keys. This script generates EdDSA key pairs and exports them in JWK format. It is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities. @@ -34,10 +31,7 @@ This module provides functions to generate secure secrets and validate base64url It ensures compatibility with JWT signing requirements. | Utility functions for JWT operations. This module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes. -It is designed to support core JWT functionalities. | JWT verification utilities. - -This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. -It supports integration with JWKS services and thumbprint pinning. +It is designed to support core JWT functionalities. @@ -53,7 +47,7 @@ It supports integration with JWKS services and thumbprint pinning. ### Code Elements
-15 code element(s) +10 code element(s) @@ -200,131 +194,6 @@ Fluent builder for creating authorization policies ---- -##### `clearJwksCache()` - -Clear the JWKS cache (for testing purposes) - - - - - - - - - - - - - - - - - - - - -
Typefunction
Visibilitypublic
Returnsvoid
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:37
- - - ---- -##### `fetchJwksFromService()` - -Fetch JWKS from a service binding -Implements 5-minute caching to reduce load on JWKS service - - - - - - - - - - - - - - - - - - - - - - - - -
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:45
- -**Parameters:** - -- `service`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").Fetcher - ---- -##### `getKeyFromJwks()` - -Find and import a specific key from JWKS by kid - - - - - - - - - - - - - - - - - - - - - - - - -
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise | CryptoKey>
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:75
- -**Parameters:** - -- `kid`: string- `jwks`: JWKWithKid[] - ---- -##### `allowedThumbprints()` - -Get allowed thumbprints for key pinning (optional security measure) - - - - - - - - - - - - - - - - - - - - -
Typefunction
Visibilitypublic
ReturnsSet
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts:104
- - - --- ##### `main()` @@ -504,40 +373,6 @@ Map OAuth scopes to permission strings - `scopes`: string[] — - List of OAuth scope strings ---- -##### `verify()` - -Verify a JWT token with HS512 or EdDSA algorithm - - - - - - - - - - - - - - - - - - - - - - - - -
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Decoded payload if valid, null otherwise
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts:28
- -**Parameters:** - -- `token`: string — - JWT token string to verify- `opts`: Partial<{ iss: string; aud: string | string[]; leeway: number; jwksService: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").Fetcher; }> — - Optional overrides for iss, aud, leeway, and jwksService - ---
diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__verify.md b/docs/architecture/chrislyons_dev_flarelette_jwt__verify.md new file mode 100644 index 0000000..fa7c4ca --- /dev/null +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__verify.md @@ -0,0 +1,133 @@ +# verify — Code View + +[← Back to Container](./chrislyons_dev_flarelette_jwt.md) | [← Back to System](./README.md) + +--- + +## Component Information + + + + + + + + + + + + + + + + + + + + +
Componentverify
Container@chrislyons-dev/flarelette-jwt
Typemodule
DescriptionJWT verification utilities. + +This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. +It supports integration with JWKS services and thumbprint pinning.
+ +--- + +## Code Structure + +### Class Diagram + +![Class Diagram](./diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.png) + +### Code Elements + +
+2 code element(s) + + + +#### Functions + +##### `resolveVerificationKey()` + +Resolve verification key from configured sources + +Implements key resolution strategy pattern: +- Strategy 1: HS512 shared secret +- Strategy 2: Inline public JWK +- Strategy 3: Service binding JWKS +- Strategy 4: HTTP JWKS URL + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilityprivate
AsyncYes
ReturnsPromise<{ key: Uint8Array | CryptoKey; algorithms: string[]; }> — Key and allowed algorithms
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts:47
+ +**Parameters:** + +- `token`: string — - JWT token string- `opts`: Partial<{ jwksService: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").Fetcher; jwksUrl: string; jwksCacheTtl: number; }> — - Verification options + +--- +##### `verify()` + +Verify a JWT token with HS512, EdDSA, or RSA algorithms + +Supports multiple key resolution strategies with automatic algorithm detection + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Decoded payload if valid, null otherwise
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts:132
+ +**Parameters:** + +- `token`: string — - JWT token string to verify- `opts`: Partial<{ iss: string; aud: string | string[]; leeway: number; jwksService: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").Fetcher; jwksUrl: string; jwksCacheTtl: number; }> — - Optional overrides for iss, aud, leeway, jwksService, jwksUrl, jwksCacheTtl + +--- + +
+ +--- + + diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__adapters.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__adapters.mmd index 9ccddb4..5c35ac0 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__adapters.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__adapters.mmd @@ -7,12 +7,12 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 41("
adapters.bindEnv
[Component: function]
Store both environment
variables and service
bindings globally
") - style 41 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 42("
adapters.getServiceBinding
[Component: function]
Get service binding by name
from global storage
") - style 42 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 43("
adapters.makeKit
[Component: function]
Returns a namespaced kit
whose calls use the provided
env bag. Automatically
injects JWKS service binding
if configured.
") - style 43 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 50("
adapters.bindEnv
[Component: function]
Store both environment
variables and service
bindings globally
") + style 50 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 51("
adapters.getServiceBinding
[Component: function]
Get service binding by name
from global storage
") + style 51 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 52("
adapters.makeKit
[Component: function]
Returns a namespaced kit
whose calls use the provided
env bag. Automatically
injects JWKS service binding
if configured.
") + style 52 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.mmd index 9bee11b..8604b32 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.mmd @@ -7,24 +7,28 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 10("
core.envMode
[Component: function]
") - style 10 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 11("
core.getCommon
[Component: function]
Get common JWT configuration
from environment Returns
partial JwtProfile-compatible
configuration
") + 11("
core.envRead
[Component: function]
") style 11 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 12("
core.getProfile
[Component: function]
Get JWT profile from
environment Returns complete
JwtProfile with detected
algorithm
") + 12("
core.envMode
[Component: function]
") style 12 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 13("
core.getHSSecret
[Component: function]
") + 13("
core.getCommon
[Component: function]
Get common JWT configuration
from environment Returns
partial JwtProfile-compatible
configuration
") style 13 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 14("
core.getPrivateJwkString
[Component: function]
") + 14("
core.getProfile
[Component: function]
Get JWT profile from
environment Returns complete
JwtProfile with detected
algorithm
") style 14 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 15("
core.getPublicJwkString
[Component: function]
") + 15("
core.getHSSecret
[Component: function]
") style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 16("
core.getJwksServiceName
[Component: function]
") + 16("
core.getPrivateJwkString
[Component: function]
") style 16 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 36("
core.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm
") - style 36 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 9("
core.envRead
[Component: function]
") - style 9 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 17("
core.getPublicJwkString
[Component: function]
") + style 17 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 18("
core.getJwksServiceName
[Component: function]
") + style 18 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 19("
core.getJwksUrl
[Component: function]
") + style 19 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 20("
core.getJwksCacheTtl
[Component: function]
") + style 20 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 44("
core.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm
") + style 44 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.mmd index 8e2a914..ee6cd86 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.mmd @@ -7,22 +7,24 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 17("
explicit.signWithConfig
[Component: function]
Sign a JWT token with
explicit configuration
") - style 17 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 18("
explicit.verifyWithConfig
[Component: function]
Verify a JWT token with
explicit configuration
") - style 18 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 19("
explicit.createTokenWithConfig
[Component: function]
Create a signed JWT token
with explicit configuration
Higher-level wrapper around
signWithConfig for
convenience.
") - style 19 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 20("
explicit.createDelegatedTokenWithConfig
[Component: function]
Create a delegated JWT token
with explicit configuration
Implements RFC 8693 actor
claim pattern for
service-to-service
delegation.
") - style 20 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 21("
explicit.checkAuthWithConfig
[Component: function]
Verify and authorize a JWT
token with explicit
configuration
") + 21("
explicit.signWithConfig
[Component: function]
Sign a JWT token with
explicit configuration
") style 21 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 22("
explicit.createHS512Config
[Component: function]
Helper function to create
HS512 config from
base64url-encoded secret
") + 22("
explicit.verifyWithConfig
[Component: function]
Verify a JWT token with
explicit configuration
") style 22 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 23("
explicit.createEdDSASignConfig
[Component: function]
Helper function to create
EdDSA sign config from JWK
") + 23("
explicit.createTokenWithConfig
[Component: function]
Create a signed JWT token
with explicit configuration
Higher-level wrapper around
signWithConfig for
convenience.
") style 23 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 24("
explicit.createEdDSAVerifyConfig
[Component: function]
Helper function to create
EdDSA verify config from JWK
") + 24("
explicit.createDelegatedTokenWithConfig
[Component: function]
Create a delegated JWT token
with explicit configuration
Implements RFC 8693 actor
claim pattern for
service-to-service
delegation.
") style 24 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 25("
explicit.checkAuthWithConfig
[Component: function]
Verify and authorize a JWT
token with explicit
configuration
") + style 25 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 26("
explicit.createHS512Config
[Component: function]
Helper function to create
HS512 config from
base64url-encoded secret
") + style 26 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 27("
explicit.createEdDSASignConfig
[Component: function]
Helper function to create
EdDSA sign config from JWK
") + style 27 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 28("
explicit.createEdDSAVerifyConfig
[Component: function]
Helper function to create
EdDSA verify config from JWK
") + style 28 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 29("
explicit.createJWKSUrlVerifyConfig
[Component: function]
Helper function to create
HTTP JWKS URL verification
config Enables testing
without environment variables
by providing explicit
configuration
") + style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.mmd new file mode 100644 index 0000000..850c013 --- /dev/null +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.mmd @@ -0,0 +1,26 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["flarelette-jwt-kit - @chrislyons-dev/flarelette-jwt - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 2 ["@chrislyons-dev/flarelette-jwt"] + style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 + + 34("
jwks.clearJwksCache
[Component: function]
Clear the JWKS cache (for
testing purposes)
") + style 34 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 35("
jwks.clearHttpJwksCache
[Component: function]
Clear the HTTP JWKS cache
(for testing purposes)
") + style 35 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 36("
jwks.fetchJwksFromService
[Component: function]
Fetch JWKS from a service
binding Implements 5-minute
caching to reduce load on
JWKS service
") + style 36 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 37("
jwks.validateJwksUrl
[Component: function]
Validate JWKS URL for
security requirements
Requirements: - Must be valid
URL format - Must use HTTPS
(except
localhost/127.0.0.1/[::1] for
testing)
") + style 37 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 38("
jwks.fetchJwksFromUrl
[Component: function]
Fetch JWKS from HTTP URL with
caching Implements
configurable TTL caching
(default 5 minutes) Security:
HTTPS-only (except
localhost), 5-second timeout,
100KB size limit
") + style 38 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 39("
jwks.getKeyFromJwks
[Component: function]
Find and import a specific
key from JWKS by kid Supports
both EdDSA (Ed25519) and RSA
(RS256/RS384/RS512) keys
Algorithm is auto-detected
from key type (kty) and curve
(crv)
") + style 39 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 40("
jwks.allowedThumbprints
[Component: function]
Get allowed thumbprints for
key pinning (optional
security measure)
") + style 40 fill:#85bbf0,stroke:#5d82a8,color:#000000 + end + + end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.mmd index 87ad6a6..7145f66 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.mmd @@ -7,36 +7,26 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 25("
util.createToken
[Component: function]
Create a signed JWT token
with optional claims
") - style 25 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 26("
util.createDelegatedToken
[Component: function]
Create a delegated JWT token
following RFC 8693 actor
claim pattern Mints a new
short-lived token for use
within service boundaries
where a service acts on
behalf of the original end
user. This implements
zero-trust delegation: -
Preserves original user
identity (sub) and
permissions - Identifies the
acting service via 'act'
claim - Prevents permission
escalation by copying
original permissions Pattern:
"I'm doing
work on behalf of user>"
") - style 26 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 27("
util.checkAuth
[Component: function]
Verify and authorize a JWT
token with policy enforcement
") - style 27 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 28("
util.policy
[Component: function]
Fluent builder for creating
authorization policies
") - style 28 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 29("
util.clearJwksCache
[Component: function]
Clear the JWKS cache (for
testing purposes)
") - style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 30("
util.fetchJwksFromService
[Component: function]
Fetch JWKS from a service
binding Implements 5-minute
caching to reduce load on
JWKS service
") + 30("
util.createToken
[Component: function]
Create a signed JWT token
with optional claims
") style 30 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 31("
util.getKeyFromJwks
[Component: function]
Find and import a specific
key from JWKS by kid
") + 31("
util.createDelegatedToken
[Component: function]
Create a delegated JWT token
following RFC 8693 actor
claim pattern Mints a new
short-lived token for use
within service boundaries
where a service acts on
behalf of the original end
user. This implements
zero-trust delegation: -
Preserves original user
identity (sub) and
permissions - Identifies the
acting service via 'act'
claim - Prevents permission
escalation by copying
original permissions Pattern:
"I'm doing
work on behalf of user>"
") style 31 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 32("
util.allowedThumbprints
[Component: function]
Get allowed thumbprints for
key pinning (optional
security measure)
") + 32("
util.checkAuth
[Component: function]
Verify and authorize a JWT
token with policy enforcement
") style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 33("
util.main
[Component: function]
") + 33("
util.policy
[Component: function]
Fluent builder for creating
authorization policies
") style 33 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 34("
util.generateSecret
[Component: function]
") - style 34 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 35("
util.isValidBase64UrlSecret
[Component: function]
") - style 35 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 37("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification
") - style 37 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 38("
util.isExpiringSoon
[Component: function]
Check if JWT payload will
expire within specified
seconds
") - style 38 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 39("
util.mapScopesToPermissions
[Component: function]
Map OAuth scopes to
permission strings
") - style 39 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 40("
util.verify
[Component: function]
Verify a JWT token with HS512
or EdDSA algorithm
") - style 40 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 41("
util.main
[Component: function]
") + style 41 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 42("
util.generateSecret
[Component: function]
") + style 42 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 43("
util.isValidBase64UrlSecret
[Component: function]
") + style 43 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 45("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification
") + style 45 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 46("
util.isExpiringSoon
[Component: function]
Check if JWT payload will
expire within specified
seconds
") + style 46 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 47("
util.mapScopesToPermissions
[Component: function]
Map OAuth scopes to
permission strings
") + style 47 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.mmd new file mode 100644 index 0000000..de12492 --- /dev/null +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.mmd @@ -0,0 +1,16 @@ +graph TB + linkStyle default fill:#ffffff + + subgraph diagram ["flarelette-jwt-kit - @chrislyons-dev/flarelette-jwt - Components"] + style diagram fill:#ffffff,stroke:#ffffff + + subgraph 2 ["@chrislyons-dev/flarelette-jwt"] + style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 + + 48("
verify.resolveVerificationKey
[Component: function]
Resolve verification key from
configured sources Implements
key resolution strategy
pattern: - Strategy 1: HS512
shared secret - Strategy 2:
Inline public JWK - Strategy
3: Service binding JWKS -
Strategy 4: HTTP JWKS URL
") + style 48 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 49("
verify.verify
[Component: function]
Verify a JWT token with
HS512, EdDSA, or RSA
algorithms Supports multiple
key resolution strategies
with automatic algorithm
detection
") + style 49 fill:#85bbf0,stroke:#5d82a8,color:#000000 + end + + end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__adapters.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__adapters.mmd index fcb5a85..0ac0ab7 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__adapters.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__adapters.mmd @@ -4,11 +4,11 @@ graph TB subgraph diagram ["flarelette-jwt-kit - flarelette-jwt - Components"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 49 ["flarelette-jwt"] - style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 60 ["flarelette-jwt"] + style 60 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 54("
adapters.apply_env_bindings
[Component: function]
Copy a Cloudflare Worker
`env` mapping into os.environ
so the kit can read it.
") - style 54 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 65("
adapters.apply_env_bindings
[Component: function]
Copy a Cloudflare Worker
`env` mapping into os.environ
so the kit can read it.
") + style 65 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__explicit.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__explicit.mmd index e5cdfe2..0db7663 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__explicit.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__explicit.mmd @@ -4,45 +4,45 @@ graph TB subgraph diagram ["flarelette-jwt-kit - flarelette-jwt - Components"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 49 ["flarelette-jwt"] - style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 60 ["flarelette-jwt"] + style 60 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 69("
explicit.BaseJwtConfig
[Component: class]
Base JWT configuration shared
by HS512 and EdDSA modes.
") - style 69 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 70("
explicit.HS512Config
[Component: class]
HS512 (HMAC-SHA512) symmetric
configuration.
") - style 70 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 71("
explicit.EdDSASignConfig
[Component: class]
EdDSA (Ed25519) asymmetric
configuration for signing.
") - style 71 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 72("
explicit.EdDSAVerifyConfig
[Component: class]
EdDSA (Ed25519) asymmetric
configuration for
verification.
") - style 72 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 73("
explicit.AuthzOptsWithConfig
[Component: class]
Authorization options for
check_auth_with_config.
") - style 73 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 74("
explicit.AuthUser
[Component: class]
Authenticated user
information.
") - style 74 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 75("
explicit._b64url
[Component: function]
Encode bytes to base64url
without padding.
") - style 75 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 76("
explicit._b64url_decode
[Component: function]
Decode base64url string (with
or without padding).
") - style 76 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 77("
explicit.sign_with_config
[Component: function]
Sign a JWT token with
explicit configuration.
") - style 77 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 78("
explicit.verify_with_config
[Component: function]
Verify a JWT token with
explicit configuration.
") - style 78 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 79("
explicit.create_token_with_config
[Component: function]
Create a signed JWT token
with explicit configuration.
") - style 79 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 80("
explicit.create_delegated_token_with_config
[Component: function]
Create a delegated JWT token
with explicit configuration.
") + 80("
explicit.BaseJwtConfig
[Component: class]
Base JWT configuration shared
by HS512 and EdDSA modes.
") style 80 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 81("
explicit.check_auth_with_config
[Component: function]
Verify and authorize a JWT
token with explicit
configuration.
") + 81("
explicit.HS512Config
[Component: class]
HS512 (HMAC-SHA512) symmetric
configuration.
") style 81 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 82("
explicit.create_hs512_config
[Component: function]
Helper function to create
HS512 config from
base64url-encoded secret.
") + 82("
explicit.EdDSASignConfig
[Component: class]
EdDSA (Ed25519) asymmetric
configuration for signing.
") style 82 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 83("
explicit.create_eddsa_sign_config
[Component: function]
Helper function to create
EdDSA sign config from JWK.
") + 83("
explicit.EdDSAVerifyConfig
[Component: class]
EdDSA (Ed25519) asymmetric
configuration for
verification.
") style 83 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 84("
explicit.create_eddsa_verify_config
[Component: function]
Helper function to create
EdDSA verify config from JWK.
") + 84("
explicit.AuthzOptsWithConfig
[Component: class]
Authorization options for
check_auth_with_config.
") style 84 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 85("
explicit.SignConfig
[Component: type]
") + 85("
explicit.AuthUser
[Component: class]
Authenticated user
information.
") style 85 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 86("
explicit.VerifyConfig
[Component: type]
") + 86("
explicit._b64url
[Component: function]
Encode bytes to base64url
without padding.
") style 86 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 87("
explicit._b64url_decode
[Component: function]
Decode base64url string (with
or without padding).
") + style 87 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 88("
explicit.sign_with_config
[Component: function]
Sign a JWT token with
explicit configuration.
") + style 88 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 89("
explicit.verify_with_config
[Component: function]
Verify a JWT token with
explicit configuration.
") + style 89 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 90("
explicit.create_token_with_config
[Component: function]
Create a signed JWT token
with explicit configuration.
") + style 90 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 91("
explicit.create_delegated_token_with_config
[Component: function]
Create a delegated JWT token
with explicit configuration.
") + style 91 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 92("
explicit.check_auth_with_config
[Component: function]
Verify and authorize a JWT
token with explicit
configuration.
") + style 92 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 93("
explicit.create_hs512_config
[Component: function]
Helper function to create
HS512 config from
base64url-encoded secret.
") + style 93 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 94("
explicit.create_eddsa_sign_config
[Component: function]
Helper function to create
EdDSA sign config from JWK.
") + style 94 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 95("
explicit.create_eddsa_verify_config
[Component: function]
Helper function to create
EdDSA verify config from JWK.
") + style 95 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 96("
explicit.SignConfig
[Component: type]
") + style 96 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 97("
explicit.VerifyConfig
[Component: type]
") + style 97 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__util.mmd b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__util.mmd index 0ce71f9..a6caace 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__util.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__util.mmd @@ -4,100 +4,100 @@ graph TB subgraph diagram ["flarelette-jwt-kit - flarelette-jwt - Components"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 49 ["flarelette-jwt"] - style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 60 ["flarelette-jwt"] + style 60 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 100("
util.Builder.roles_all
[Component: method]
") + 100("
util.PolicyBuilder.base
[Component: method]
") style 100 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 101("
util.Builder.roles_any
[Component: method]
") + 101("
util.PolicyBuilder.need_all
[Component: method]
") style 101 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 102("
util.Builder.where
[Component: method]
") + 102("
util.PolicyBuilder.need_any
[Component: method]
") style 102 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 103("
util.Builder.build
[Component: method]
") + 103("
util.PolicyBuilder.roles_all
[Component: method]
") style 103 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 104("
util.create_token
[Component: function]
Create a signed JWT token
with optional claims.
") + 104("
util.PolicyBuilder.roles_any
[Component: method]
") style 104 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 105("
util.create_delegated_token
[Component: function]
Create a delegated JWT token
following RFC 8693 actor
claim pattern.
") + 105("
util.PolicyBuilder.where
[Component: method]
") style 105 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 106("
util.check_auth
[Component: function]
Verify and authorize a JWT
token with policy
enforcement.
") + 106("
util.PolicyBuilder.build
[Component: method]
") style 106 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 107("
util.policy
[Component: function]
Fluent builder for creating
authorization policies.
") + 107("
util.Builder
[Component: class]
") style 107 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 108("
util.generate_secret
[Component: function]
") + 108("
util.Builder.base
[Component: method]
") style 108 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 109("
util.is_valid_base64url_secret
[Component: function]
") + 109("
util.Builder.need_all
[Component: method]
") style 109 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 110("
util.main
[Component: function]
") + 110("
util.Builder.need_any
[Component: method]
") style 110 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 111("
util._b64url
[Component: function]
") + 111("
util.Builder.roles_all
[Component: method]
") style 111 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 112("
util.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm.
") + 112("
util.Builder.roles_any
[Component: method]
") style 112 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 113("
util.ParsedJwt
[Component: class]
Parsed JWT token structure.
") + 113("
util.Builder.where
[Component: method]
") style 113 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 114("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification.
") + 114("
util.Builder.build
[Component: method]
") style 114 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 115("
util.is_expiring_soon
[Component: function]
Check if JWT payload will
expire within specified
seconds.
") + 115("
util.create_token
[Component: function]
Create a signed JWT token
with optional claims.
") style 115 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 116("
util.map_scopes_to_permissions
[Component: function]
Map OAuth scopes to
permission strings.
") + 116("
util.create_delegated_token
[Component: function]
Create a delegated JWT token
following RFC 8693 actor
claim pattern.
") style 116 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 117("
util._b64url_decode
[Component: function]
") + 117("
util.check_auth
[Component: function]
Verify and authorize a JWT
token with policy
enforcement.
") style 117 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 118("
util.verify
[Component: function]
Verify a JWT token with HS512
or EdDSA algorithm.
") + 118("
util.policy
[Component: function]
Fluent builder for creating
authorization policies.
") style 118 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 55("
util.JwtHeader
[Component: class]
JWT token header structure.
") - style 55 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 56("
util.ActorClaim
[Component: class]
Actor claim for service
delegation (RFC 8693).
") - style 56 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 57("
util.JwtPayload
[Component: class]
JWT token payload/claims
structure.
") - style 57 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 58("
util.JwtProfile
[Component: class]
JWT Profile structure
matching
flarelette-jwt.profile.schema.json.
") - style 58 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 59("
util.JwtCommonConfig
[Component: class]
Common JWT configuration from
environment variables.
") - style 59 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 60("
util.mode
[Component: function]
Detect JWT algorithm mode
from environment variables
based on role.
") - style 60 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 61("
util.common
[Component: function]
Get common JWT configuration
from environment.
") - style 61 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 62("
util.profile
[Component: function]
Get JWT profile from
environment.
") - style 62 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 63("
util._get_indirect
[Component: function]
") - style 63 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 64("
util.get_hs_secret_bytes
[Component: function]
") - style 64 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 65("
util.get_public_jwk_string
[Component: function]
") - style 65 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 66("
util.AlgType
[Component: type]
") + 119("
util.generate_secret
[Component: function]
") + style 119 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 120("
util.is_valid_base64url_secret
[Component: function]
") + style 120 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 121("
util.main
[Component: function]
") + style 121 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 122("
util._b64url
[Component: function]
") + style 122 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 123("
util.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm.
") + style 123 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 124("
util.ParsedJwt
[Component: class]
Parsed JWT token structure.
") + style 124 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 125("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification.
") + style 125 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 126("
util.is_expiring_soon
[Component: function]
Check if JWT payload will
expire within specified
seconds.
") + style 126 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 127("
util.map_scopes_to_permissions
[Component: function]
Map OAuth scopes to
permission strings.
") + style 127 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 128("
util._b64url_decode
[Component: function]
") + style 128 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 129("
util.verify
[Component: function]
Verify a JWT token with HS512
or EdDSA algorithm.
") + style 129 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 66("
util.JwtHeader
[Component: class]
JWT token header structure.
") style 66 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 67("
util.JwtValue
[Component: type]
") + 67("
util.ActorClaim
[Component: class]
Actor claim for service
delegation (RFC 8693).
") style 67 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 68("
util.ClaimsDict
[Component: type]
") + 68("
util.JwtPayload
[Component: class]
JWT token payload/claims
structure.
") style 68 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 87("
util.AuthUser
[Component: class]
Authenticated user
information returned by
check_auth.
") - style 87 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 88("
util.PolicyBuilder
[Component: class]
Builder interface for
creating JWT authorization
policies.
") - style 88 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 89("
util.PolicyBuilder.base
[Component: method]
") - style 89 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 90("
util.PolicyBuilder.need_all
[Component: method]
") - style 90 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 91("
util.PolicyBuilder.need_any
[Component: method]
") - style 91 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 92("
util.PolicyBuilder.roles_all
[Component: method]
") - style 92 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 93("
util.PolicyBuilder.roles_any
[Component: method]
") - style 93 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 94("
util.PolicyBuilder.where
[Component: method]
") - style 94 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 95("
util.PolicyBuilder.build
[Component: method]
") - style 95 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 96("
util.Builder
[Component: class]
") - style 96 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 97("
util.Builder.base
[Component: method]
") - style 97 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 98("
util.Builder.need_all
[Component: method]
") + 69("
util.JwtProfile
[Component: class]
JWT Profile structure
matching
flarelette-jwt.profile.schema.json.
") + style 69 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 70("
util.JwtCommonConfig
[Component: class]
Common JWT configuration from
environment variables.
") + style 70 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 71("
util.mode
[Component: function]
Detect JWT algorithm mode
from environment variables
based on role.
") + style 71 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 72("
util.common
[Component: function]
Get common JWT configuration
from environment.
") + style 72 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 73("
util.profile
[Component: function]
Get JWT profile from
environment.
") + style 73 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 74("
util._get_indirect
[Component: function]
") + style 74 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 75("
util.get_hs_secret_bytes
[Component: function]
") + style 75 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 76("
util.get_public_jwk_string
[Component: function]
") + style 76 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 77("
util.AlgType
[Component: type]
") + style 77 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 78("
util.JwtValue
[Component: type]
") + style 78 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 79("
util.ClaimsDict
[Component: type]
") + style 79 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 98("
util.AuthUser
[Component: class]
Authenticated user
information returned by
check_auth.
") style 98 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 99("
util.Builder.need_any
[Component: method]
") + 99("
util.PolicyBuilder
[Component: class]
Builder interface for
creating JWT authorization
policies.
") style 99 fill:#85bbf0,stroke:#5d82a8,color:#000000 end diff --git a/docs/architecture/diagrams/mermaid/structurizr-Components__chrislyons_dev_flarelette_jwt.mmd b/docs/architecture/diagrams/mermaid/structurizr-Components__chrislyons_dev_flarelette_jwt.mmd index c781b3b..f38b815 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Components__chrislyons_dev_flarelette_jwt.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Components__chrislyons_dev_flarelette_jwt.mmd @@ -7,23 +7,29 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 + 10("
adapters
[Component: module]
Component inferred from
directory: adapters
") + style 10 fill:#85bbf0,stroke:#5d82a8,color:#000000 3("
core
[Component: module]
CLI utility for generating
JWT secrets. This script
provides options to generate
secrets in various formats,
including JSON and dotenv. It
is designed to be executed as
a standalone Node.js script.
| Configuration utilities for
JWT operations. This module
provides functions to read
environment variables and
derive JWT-related
configurations. It includes
support for both symmetric
(HS512) and asymmetric
(EdDSA) algorithms. | JWT
signing utilities. This
module provides functions to
sign JWT tokens using either
HS512 or EdDSA algorithms. It
supports custom claims and
configuration overrides.
") style 3 fill:#85bbf0,stroke:#5d82a8,color:#000000 4("
explicit
[Component: module]
Explicit configuration API
for JWT operations. This
module provides functions
that accept explicit
configuration objects instead
of relying on environment
variables or global state.
Use this API when you need
full control over
configuration, especially in
development environments or
when working with multiple
JWT configurations.
") style 4 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 5("
util
[Component: module]
High-level JWT utilities for
creating, delegating,
verifying, and authorizing
JWT tokens | JSON Web Key Set
(JWKS) utilities. This module
provides functions to fetch
and manage JWKS, including
caching and key lookup by key
ID (kid). It supports
integration with external
JWKS services. | Key
generation utility for EdDSA
keys. This script generates
EdDSA key pairs and exports
them in JWK format. It is
designed to be executed as a
standalone Node.js script. |
Secret generation and
validation utilities. This
module provides functions to
generate secure secrets and
validate base64url-encoded
secrets. It ensures
compatibility with JWT
signing requirements. |
Utility functions for JWT
operations. This module
provides helper functions for
parsing JWTs, checking
expiration, and mapping OAuth
scopes. It is designed to
support core JWT
functionalities. | JWT
verification utilities. This
module provides functions to
verify JWT tokens using
either HS512 or EdDSA
algorithms. It supports
integration with JWKS
services and thumbprint
pinning.
") + 5("
util
[Component: module]
High-level JWT utilities for
creating, delegating,
verifying, and authorizing
JWT tokens | Key generation
utility for EdDSA keys. This
script generates EdDSA key
pairs and exports them in JWK
format. It is designed to be
executed as a standalone
Node.js script. | Secret
generation and validation
utilities. This module
provides functions to
generate secure secrets and
validate base64url-encoded
secrets. It ensures
compatibility with JWT
signing requirements. |
Utility functions for JWT
operations. This module
provides helper functions for
parsing JWTs, checking
expiration, and mapping OAuth
scopes. It is designed to
support core JWT
functionalities.
") style 5 fill:#85bbf0,stroke:#5d82a8,color:#000000 6("
main
[Component: module]
Entry point for the
flarelette-jwt library. This
module re-exports core
functionalities, including
signing, verification,
utilities, and type
definitions. It serves as the
main interface for library
consumers.
") style 6 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 7("
types
[Component: module]
Type definitions for JWT
operations. This module
defines types for JWT
headers, payloads, profiles,
and related structures. It
ensures type safety and
consistency across the
library.
") + 7("
jwks
[Component: module]
JSON Web Key Set (JWKS)
utilities. This module
provides functions to fetch
and manage JWKS, including
caching and key lookup by key
ID (kid). It supports
integration with external
JWKS services.
") style 7 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 8("
adapters
[Component: module]
Component inferred from
directory: adapters
") + 8("
types
[Component: module]
Type definitions for JWT
operations. This module
defines types for JWT
headers, payloads, profiles,
and related structures. It
ensures type safety and
consistency across the
library.
") style 8 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 9("
verify
[Component: module]
JWT verification utilities.
This module provides
functions to verify JWT
tokens using either HS512 or
EdDSA algorithms. It supports
integration with JWKS
services and thumbprint
pinning.
") + style 9 fill:#85bbf0,stroke:#5d82a8,color:#000000 end - 5-- "
ParsedJwt | JwtPayload |
AlgType | Fetcher
" -->7 - 5-- "
envMode | getCommon |
getHSSecret |
getPublicJwkString
" -->3 - 8-- "
imports * as kit
" -->6 - 8-- "
imports getJwksServiceName
" -->3 - 8-- "
WorkerEnv | Fetcher
" -->7 + 5-- "
ParsedJwt | JwtPayload
" -->8 + 9-- "
envMode | getCommon |
getHSSecret |
getPublicJwkString |
getJwksUrl | getJwksCacheTtl
" -->3 + 9-- "
fetchJwksFromService |
fetchJwksFromUrl |
getKeyFromJwks |
allowedThumbprints
" -->7 + 9-- "
AlgType | Fetcher |
JwtPayload
" -->8 + 10-- "
imports * as kit
" -->6 + 10-- "
imports getJwksServiceName
" -->3 + 10-- "
WorkerEnv | Fetcher
" -->8 end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Components_flarelette_jwt.mmd b/docs/architecture/diagrams/mermaid/structurizr-Components_flarelette_jwt.mmd index 324fb12..bfd601c 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Components_flarelette_jwt.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Components_flarelette_jwt.mmd @@ -4,17 +4,17 @@ graph TB subgraph diagram ["flarelette-jwt-kit - flarelette-jwt - Components"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 49 ["flarelette-jwt"] - style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 60 ["flarelette-jwt"] + style 60 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 50("
adapters
[Component: module]
Adapters for Cloudflare
Workers Environment This
module provides utilities to
adapt Cloudflare Workers
environment variables for use
with the Flarelette JWT
library.
") - style 50 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 51("
util
[Component: module]
Environment Configuration for
JWT Operations This module
provides functions to read
environment variables and
derive JWT-related
configurations. It supports
both symmetric (HS512) and
asymmetric (EdDSA)
algorithms.
") - style 51 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 52("
explicit
[Component: module]
Explicit Configuration API
for JWT Operations This
module provides functions
that accept explicit
configuration objects instead
of relying on environment
variables or global state.
Use this API when you need
full control over
configuration, especially in
development environments or
when working with multiple
JWT configurations.
") - style 52 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 53("
flarelette_jwt
[Component: module]
Component derived from
directory: flarelette_jwt
") - style 53 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 61("
adapters
[Component: module]
Adapters for Cloudflare
Workers Environment This
module provides utilities to
adapt Cloudflare Workers
environment variables for use
with the Flarelette JWT
library.
") + style 61 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 62("
util
[Component: module]
Environment Configuration for
JWT Operations This module
provides functions to read
environment variables and
derive JWT-related
configurations. It supports
both symmetric (HS512) and
asymmetric (EdDSA)
algorithms.
") + style 62 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 63("
explicit
[Component: module]
Explicit Configuration API
for JWT Operations This
module provides functions
that accept explicit
configuration objects instead
of relying on environment
variables or global state.
Use this API when you need
full control over
configuration, especially in
development environments or
when working with multiple
JWT configurations.
") + style 63 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 64("
flarelette_jwt
[Component: module]
Component derived from
directory: flarelette_jwt
") + style 64 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file diff --git a/docs/architecture/diagrams/mermaid/structurizr-Containers.mmd b/docs/architecture/diagrams/mermaid/structurizr-Containers.mmd index f4555e3..083e179 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Containers.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Containers.mmd @@ -9,8 +9,8 @@ graph TB 2("
@chrislyons-dev/flarelette-jwt
[Container: Service]
Environment-driven JWT
authentication for Cloudflare
Workers with secret-name
indirection
") style 2 fill:#438dd5,stroke:#2e6295,color:#ffffff - 49("
flarelette-jwt
[Container: Service]
Environment-driven JWT
authentication for Cloudflare
Workers Python with
secret-name indirection
") - style 49 fill:#438dd5,stroke:#2e6295,color:#ffffff + 60("
flarelette-jwt
[Container: Service]
Environment-driven JWT
authentication for Cloudflare
Workers Python with
secret-name indirection
") + style 60 fill:#438dd5,stroke:#2e6295,color:#ffffff end end \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.puml index e2e04d3..a6abeb9 100644 --- a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.puml +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.puml @@ -44,6 +44,13 @@ skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -51,6 +58,13 @@ skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -86,6 +100,7 @@ skinparam rectangle<> { } rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" <> { + rectangle "==core.envRead\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coreenvRead rectangle "==core.envMode\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coreenvMode rectangle "==core.getCommon\n[Component: function]\n\nGet common JWT configuration from environment Returns partial JwtProfile-compatible configuration" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetCommon rectangle "==core.getProfile\n[Component: function]\n\nGet JWT profile from environment Returns complete JwtProfile with detected algorithm" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetProfile @@ -93,8 +108,9 @@ rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" rectangle "==core.getPrivateJwkString\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetPrivateJwkString rectangle "==core.getPublicJwkString\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetPublicJwkString rectangle "==core.getJwksServiceName\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetJwksServiceName + rectangle "==core.getJwksUrl\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetJwksUrl + rectangle "==core.getJwksCacheTtl\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetJwksCacheTtl rectangle "==core.sign\n[Component: function]\n\nSign a JWT token with HS512 or EdDSA algorithm" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coresign - rectangle "==core.envRead\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coreenvRead } @enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.puml index ba43262..08d212e 100644 --- a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.puml +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.puml @@ -51,6 +51,13 @@ skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -87,6 +94,7 @@ rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" rectangle "==explicit.createHS512Config\n[Component: function]\n\nHelper function to create HS512 config from base64url-encoded secret" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcreateHS512Config rectangle "==explicit.createEdDSASignConfig\n[Component: function]\n\nHelper function to create EdDSA sign config from JWK" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcreateEdDSASignConfig rectangle "==explicit.createEdDSAVerifyConfig\n[Component: function]\n\nHelper function to create EdDSA verify config from JWK" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcreateEdDSAVerifyConfig + rectangle "==explicit.createJWKSUrlVerifyConfig\n[Component: function]\n\nHelper function to create HTTP JWKS URL verification config Enables testing without environment variables by providing explicit configuration" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcreateJWKSUrlVerifyConfig } @enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks-key.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks-key.puml new file mode 100644 index 0000000..da75bcd --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks-key.puml @@ -0,0 +1,29 @@ +@startuml +set separator none + +skinparam { + shadowing false + arrowFontSize 15 + defaultTextAlignment center + wrapWidth 100 + maxMessageSize 100 + defaultFontName "Arial" +} +hide stereotype + +skinparam rectangle<<_transparent>> { + BorderColor transparent + BackgroundColor transparent + FontColor transparent +} + +skinparam rectangle<<1>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 +} +rectangle "==Component" <<1>> + + +@enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.puml new file mode 100644 index 0000000..a047d39 --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.puml @@ -0,0 +1,84 @@ +@startuml +set separator none +title flarelette-jwt-kit - @chrislyons-dev/flarelette-jwt - Components + +top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 + defaultFontName "Arial" +} + +hide stereotype + +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BorderColor #2e6295 + FontColor #2e6295 + shadowing false +} + +rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" <> { + rectangle "==jwks.clearJwksCache\n[Component: function]\n\nClear the JWKS cache (for testing purposes)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksclearJwksCache + rectangle "==jwks.clearHttpJwksCache\n[Component: function]\n\nClear the HTTP JWKS cache (for testing purposes)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksclearHttpJwksCache + rectangle "==jwks.fetchJwksFromService\n[Component: function]\n\nFetch JWKS from a service binding Implements 5-minute caching to reduce load on JWKS service" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksfetchJwksFromService + rectangle "==jwks.validateJwksUrl\n[Component: function]\n\nValidate JWKS URL for security requirements Requirements: - Must be valid URL format - Must use HTTPS (except localhost/127.0.0.1/[::1] for testing)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksvalidateJwksUrl + rectangle "==jwks.fetchJwksFromUrl\n[Component: function]\n\nFetch JWKS from HTTP URL with caching Implements configurable TTL caching (default 5 minutes) Security: HTTPS-only (except localhost), 5-second timeout, 100KB size limit" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksfetchJwksFromUrl + rectangle "==jwks.getKeyFromJwks\n[Component: function]\n\nFind and import a specific key from JWKS by kid Supports both EdDSA (Ed25519) and RSA (RS256/RS384/RS512) keys Algorithm is auto-detected from key type (kty) and curve (crv)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksgetKeyFromJwks + rectangle "==jwks.allowedThumbprints\n[Component: function]\n\nGet allowed thumbprints for key pinning (optional security measure)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwksallowedThumbprints +} + +@enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.puml index 000d6e2..4cac45f 100644 --- a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.puml +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.puml @@ -16,13 +16,6 @@ skinparam { hide stereotype -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - roundCorner 20 - shadowing false -} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -30,13 +23,6 @@ skinparam rectangle<> roundCorner 20 shadowing false } -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - roundCorner 20 - shadowing false -} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -51,13 +37,6 @@ skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - roundCorner 20 - shadowing false -} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -65,13 +44,6 @@ skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - roundCorner 20 - shadowing false -} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -114,13 +86,6 @@ skinparam rectangle<> { roundCorner 20 shadowing false } -skinparam rectangle<> { - BackgroundColor #85bbf0 - FontColor #000000 - BorderColor #5d82a8 - roundCorner 20 - shadowing false -} skinparam rectangle<> { BorderColor #2e6295 FontColor #2e6295 @@ -132,17 +97,12 @@ rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" rectangle "==util.createDelegatedToken\n[Component: function]\n\nCreate a delegated JWT token following RFC 8693 actor claim pattern Mints a new short-lived token for use within service boundaries where a service acts on behalf of the original end user. This implements zero-trust delegation: - Preserves original user identity (sub) and permissions - Identifies the acting service via 'act' claim - Prevents permission escalation by copying original permissions Pattern: "I'm doing work on behalf of "" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilcreateDelegatedToken rectangle "==util.checkAuth\n[Component: function]\n\nVerify and authorize a JWT token with policy enforcement" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilcheckAuth rectangle "==util.policy\n[Component: function]\n\nFluent builder for creating authorization policies" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilpolicy - rectangle "==util.clearJwksCache\n[Component: function]\n\nClear the JWKS cache (for testing purposes)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilclearJwksCache - rectangle "==util.fetchJwksFromService\n[Component: function]\n\nFetch JWKS from a service binding Implements 5-minute caching to reduce load on JWKS service" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilfetchJwksFromService - rectangle "==util.getKeyFromJwks\n[Component: function]\n\nFind and import a specific key from JWKS by kid" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilgetKeyFromJwks - rectangle "==util.allowedThumbprints\n[Component: function]\n\nGet allowed thumbprints for key pinning (optional security measure)" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilallowedThumbprints rectangle "==util.main\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilmain rectangle "==util.generateSecret\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilgenerateSecret rectangle "==util.isValidBase64UrlSecret\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilisValidBase64UrlSecret rectangle "==util.parse\n[Component: function]\n\nParse a JWT token into header and payload without verification" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilparse rectangle "==util.isExpiringSoon\n[Component: function]\n\nCheck if JWT payload will expire within specified seconds" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilisExpiringSoon rectangle "==util.mapScopesToPermissions\n[Component: function]\n\nMap OAuth scopes to permission strings" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilmapScopesToPermissions - rectangle "==util.verify\n[Component: function]\n\nVerify a JWT token with HS512 or EdDSA algorithm" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.utilverify } @enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify-key.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify-key.puml new file mode 100644 index 0000000..da75bcd --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify-key.puml @@ -0,0 +1,29 @@ +@startuml +set separator none + +skinparam { + shadowing false + arrowFontSize 15 + defaultTextAlignment center + wrapWidth 100 + maxMessageSize 100 + defaultFontName "Arial" +} +hide stereotype + +skinparam rectangle<<_transparent>> { + BorderColor transparent + BackgroundColor transparent + FontColor transparent +} + +skinparam rectangle<<1>> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 +} +rectangle "==Component" <<1>> + + +@enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.puml new file mode 100644 index 0000000..50f4330 --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.puml @@ -0,0 +1,44 @@ +@startuml +set separator none +title flarelette-jwt-kit - @chrislyons-dev/flarelette-jwt - Components + +top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 + +skinparam { + arrowFontSize 10 + defaultTextAlignment center + wrapWidth 200 + maxMessageSize 100 + defaultFontName "Arial" +} + +hide stereotype + +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} +skinparam rectangle<> { + BorderColor #2e6295 + FontColor #2e6295 + shadowing false +} + +rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" <> { + rectangle "==verify.resolveVerificationKey\n[Component: function]\n\nResolve verification key from configured sources Implements key resolution strategy pattern: - Strategy 1: HS512 shared secret - Strategy 2: Inline public JWK - Strategy 3: Service binding JWKS - Strategy 4: HTTP JWKS URL" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.verifyresolveVerificationKey + rectangle "==verify.verify\n[Component: function]\n\nVerify a JWT token with HS512, EdDSA, or RSA algorithms Supports multiple key resolution strategies with automatic algorithm detection" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.verifyverify +} + +@enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__util.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__util.puml index d3775d5..64ba91f 100644 --- a/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__util.puml +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__util.puml @@ -345,6 +345,17 @@ skinparam rectangle<> { } rectangle "flarelette-jwt\n[Container: Service]" <> { + rectangle "==util.PolicyBuilder.base\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderbase + rectangle "==util.PolicyBuilder.need_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderneed_all + rectangle "==util.PolicyBuilder.need_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderneed_any + rectangle "==util.PolicyBuilder.roles_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderroles_all + rectangle "==util.PolicyBuilder.roles_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderroles_any + rectangle "==util.PolicyBuilder.where\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderwhere + rectangle "==util.PolicyBuilder.build\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderbuild + rectangle "==util.Builder\n[Component: class]" <> as flarelettejwtkit.flarelettejwt.utilBuilder + rectangle "==util.Builder.base\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderbase + rectangle "==util.Builder.need_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderneed_all + rectangle "==util.Builder.need_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderneed_any rectangle "==util.Builder.roles_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderroles_all rectangle "==util.Builder.roles_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderroles_any rectangle "==util.Builder.where\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderwhere @@ -380,17 +391,6 @@ rectangle "flarelette-jwt\n[Container: Service]" <[Component: type]" <> as flarelettejwtkit.flarelettejwt.utilClaimsDict rectangle "==util.AuthUser\n[Component: class]\n\nAuthenticated user information returned by check_auth." <> as flarelettejwtkit.flarelettejwt.utilAuthUser rectangle "==util.PolicyBuilder\n[Component: class]\n\nBuilder interface for creating JWT authorization policies." <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilder - rectangle "==util.PolicyBuilder.base\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderbase - rectangle "==util.PolicyBuilder.need_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderneed_all - rectangle "==util.PolicyBuilder.need_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderneed_any - rectangle "==util.PolicyBuilder.roles_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderroles_all - rectangle "==util.PolicyBuilder.roles_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderroles_any - rectangle "==util.PolicyBuilder.where\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderwhere - rectangle "==util.PolicyBuilder.build\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilPolicyBuilderbuild - rectangle "==util.Builder\n[Component: class]" <> as flarelettejwtkit.flarelettejwt.utilBuilder - rectangle "==util.Builder.base\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderbase - rectangle "==util.Builder.need_all\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderneed_all - rectangle "==util.Builder.need_any\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderneed_any } @enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Components__chrislyons_dev_flarelette_jwt.puml b/docs/architecture/diagrams/plantuml/structurizr-Components__chrislyons_dev_flarelette_jwt.puml index 264a373..28d0b85 100644 --- a/docs/architecture/diagrams/plantuml/structurizr-Components__chrislyons_dev_flarelette_jwt.puml +++ b/docs/architecture/diagrams/plantuml/structurizr-Components__chrislyons_dev_flarelette_jwt.puml @@ -37,6 +37,13 @@ skinparam rectangle<> { roundCorner 20 shadowing false } +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} skinparam rectangle<> { BackgroundColor #85bbf0 FontColor #000000 @@ -58,6 +65,13 @@ skinparam rectangle<> { roundCorner 20 shadowing false } +skinparam rectangle<> { + BackgroundColor #85bbf0 + FontColor #000000 + BorderColor #5d82a8 + roundCorner 20 + shadowing false +} skinparam rectangle<> { BorderColor #2e6295 FontColor #2e6295 @@ -65,16 +79,20 @@ skinparam rectangle<> { } rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" <> { + rectangle "==adapters\n[Component: module]\n\nComponent inferred from directory: adapters" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.adapters rectangle "==core\n[Component: module]\n\nCLI utility for generating JWT secrets. This script provides options to generate secrets in various formats, including JSON and dotenv. It is designed to be executed as a standalone Node.js script. | Configuration utilities for JWT operations. This module provides functions to read environment variables and derive JWT-related configurations. It includes support for both symmetric (HS512) and asymmetric (EdDSA) algorithms. | JWT signing utilities. This module provides functions to sign JWT tokens using either HS512 or EdDSA algorithms. It supports custom claims and configuration overrides." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.core rectangle "==explicit\n[Component: module]\n\nExplicit configuration API for JWT operations. This module provides functions that accept explicit configuration objects instead of relying on environment variables or global state. Use this API when you need full control over configuration, especially in development environments or when working with multiple JWT configurations." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicit - rectangle "==util\n[Component: module]\n\nHigh-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | JSON Web Key Set (JWKS) utilities. This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). It supports integration with external JWKS services. | Key generation utility for EdDSA keys. This script generates EdDSA key pairs and exports them in JWK format. It is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities. This module provides functions to generate secure secrets and validate base64url-encoded secrets. It ensures compatibility with JWT signing requirements. | Utility functions for JWT operations. This module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes. It is designed to support core JWT functionalities. | JWT verification utilities. This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. It supports integration with JWKS services and thumbprint pinning." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.util + rectangle "==util\n[Component: module]\n\nHigh-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | Key generation utility for EdDSA keys. This script generates EdDSA key pairs and exports them in JWK format. It is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities. This module provides functions to generate secure secrets and validate base64url-encoded secrets. It ensures compatibility with JWT signing requirements. | Utility functions for JWT operations. This module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes. It is designed to support core JWT functionalities." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.util rectangle "==main\n[Component: module]\n\nEntry point for the flarelette-jwt library. This module re-exports core functionalities, including signing, verification, utilities, and type definitions. It serves as the main interface for library consumers." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.main + rectangle "==jwks\n[Component: module]\n\nJSON Web Key Set (JWKS) utilities. This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). It supports integration with external JWKS services." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.jwks rectangle "==types\n[Component: module]\n\nType definitions for JWT operations. This module defines types for JWT headers, payloads, profiles, and related structures. It ensures type safety and consistency across the library." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.types - rectangle "==adapters\n[Component: module]\n\nComponent inferred from directory: adapters" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.adapters + rectangle "==verify\n[Component: module]\n\nJWT verification utilities. This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. It supports integration with JWKS services and thumbprint pinning." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.verify } -flarelettejwtkit.chrislyonsdevflarelettejwt.util -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.types : "ParsedJwt | JwtPayload | AlgType | Fetcher" -flarelettejwtkit.chrislyonsdevflarelettejwt.util -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.core : "envMode | getCommon | getHSSecret | getPublicJwkString" +flarelettejwtkit.chrislyonsdevflarelettejwt.util -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.types : "ParsedJwt | JwtPayload" +flarelettejwtkit.chrislyonsdevflarelettejwt.verify -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.core : "envMode | getCommon | getHSSecret | getPublicJwkString | getJwksUrl | getJwksCacheTtl" +flarelettejwtkit.chrislyonsdevflarelettejwt.verify -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.jwks : "fetchJwksFromService | fetchJwksFromUrl | getKeyFromJwks | allowedThumbprints" +flarelettejwtkit.chrislyonsdevflarelettejwt.verify -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.types : "AlgType | Fetcher | JwtPayload" flarelettejwtkit.chrislyonsdevflarelettejwt.adapters -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.main : "imports * as kit" flarelettejwtkit.chrislyonsdevflarelettejwt.adapters -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.core : "imports getJwksServiceName" flarelettejwtkit.chrislyonsdevflarelettejwt.adapters -[#707070,thickness=2]-> flarelettejwtkit.chrislyonsdevflarelettejwt.types : "WorkerEnv | Fetcher" diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.png index b4dee3d..d148af1 100644 Binary files a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.png and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__core.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png index bebdbb5..1c2c5d3 100644 Binary files a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks-key.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks-key.png new file mode 100644 index 0000000..d4d2249 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks-key.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.png new file mode 100644 index 0000000..c665501 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__jwks.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.png index 5c3bee4..20eb2ee 100644 Binary files a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.png and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__util.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify-key.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify-key.png new file mode 100644 index 0000000..d4d2249 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify-key.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.png new file mode 100644 index 0000000..3988c79 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__verify.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__util.png b/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__util.png index 1894b57..4042baa 100644 Binary files a/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__util.png and b/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__util.png differ diff --git a/docs/architecture/diagrams/structurizr-Components__chrislyons_dev_flarelette_jwt.png b/docs/architecture/diagrams/structurizr-Components__chrislyons_dev_flarelette_jwt.png index 4577ae0..7b08648 100644 Binary files a/docs/architecture/diagrams/structurizr-Components__chrislyons_dev_flarelette_jwt.png and b/docs/architecture/diagrams/structurizr-Components__chrislyons_dev_flarelette_jwt.png differ diff --git a/docs/architecture/flarelette-jwt-kit-ir.json b/docs/architecture/flarelette-jwt-kit-ir.json index 8f2136d..2455295 100644 --- a/docs/architecture/flarelette-jwt-kit-ir.json +++ b/docs/architecture/flarelette-jwt-kit-ir.json @@ -37,26 +37,33 @@ "type": "module" }, { - "description": "Explicit configuration API for JWT operations.\n\nThis module provides functions that accept explicit configuration objects\ninstead of relying on environment variables or global state. Use this API\nwhen you need full control over configuration, especially in development\nenvironments or when working with multiple JWT configurations.", + "description": "Explicit configuration API for JWT operations.\r\n\r\nThis module provides functions that accept explicit configuration objects\r\ninstead of relying on environment variables or global state. Use this API\r\nwhen you need full control over configuration, especially in development\r\nenvironments or when working with multiple JWT configurations.", "id": "chrislyons_dev_flarelette_jwt__explicit", "containerId": "chrislyons_dev_flarelette_jwt", "name": "explicit", "type": "module" }, { - "description": "High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | JSON Web Key Set (JWKS) utilities.\n\nThis module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid).\nIt supports integration with external JWKS services. | Key generation utility for EdDSA keys.\n\nThis script generates EdDSA key pairs and exports them in JWK format.\nIt is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities.\n\nThis module provides functions to generate secure secrets and validate base64url-encoded secrets.\nIt ensures compatibility with JWT signing requirements. | Utility functions for JWT operations.\n\nThis module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes.\nIt is designed to support core JWT functionalities. | JWT verification utilities.\n\nThis module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms.\nIt supports integration with JWKS services and thumbprint pinning.", + "description": "High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | Key generation utility for EdDSA keys.\n\nThis script generates EdDSA key pairs and exports them in JWK format.\nIt is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities.\n\nThis module provides functions to generate secure secrets and validate base64url-encoded secrets.\nIt ensures compatibility with JWT signing requirements. | Utility functions for JWT operations.\n\nThis module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes.\nIt is designed to support core JWT functionalities.", "id": "chrislyons_dev_flarelette_jwt__util", "containerId": "chrislyons_dev_flarelette_jwt", "name": "util", "type": "module" }, { - "description": "Entry point for the flarelette-jwt library.\n\nThis module re-exports core functionalities, including signing, verification, utilities, and type definitions.\nIt serves as the main interface for library consumers.", + "description": "Entry point for the flarelette-jwt library.\r\n\r\nThis module re-exports core functionalities, including signing, verification, utilities, and type definitions.\r\nIt serves as the main interface for library consumers.", "id": "chrislyons_dev_flarelette_jwt__main", "containerId": "chrislyons_dev_flarelette_jwt", "name": "main", "type": "module" }, + { + "description": "JSON Web Key Set (JWKS) utilities.\n\nThis module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid).\nIt supports integration with external JWKS services.", + "id": "chrislyons_dev_flarelette_jwt__jwks", + "containerId": "chrislyons_dev_flarelette_jwt", + "name": "jwks", + "type": "module" + }, { "description": "Type definitions for JWT operations.\n\nThis module defines types for JWT headers, payloads, profiles, and related structures.\nIt ensures type safety and consistency across the library.", "id": "chrislyons_dev_flarelette_jwt__types", @@ -64,6 +71,13 @@ "name": "types", "type": "module" }, + { + "description": "JWT verification utilities.\n\nThis module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms.\nIt supports integration with JWKS services and thumbprint pinning.", + "id": "chrislyons_dev_flarelette_jwt__verify", + "containerId": "chrislyons_dev_flarelette_jwt", + "name": "verify", + "type": "module" + }, { "description": "Component inferred from directory: adapters", "id": "chrislyons_dev_flarelette_jwt__adapters", @@ -151,7 +165,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", - "lineNumber": 54 + "lineNumber": 65 }, { "description": "Get JWT profile from environment\nReturns complete JwtProfile with detected algorithm", @@ -173,7 +187,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", - "lineNumber": 67 + "lineNumber": 78 }, { "id": "chrislyons_dev_flarelette_jwt__core__gethssecret", @@ -185,7 +199,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", - "lineNumber": 82 + "lineNumber": 93 }, { "id": "chrislyons_dev_flarelette_jwt__core__getprivatejwkstring", @@ -197,7 +211,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", - "lineNumber": 109 + "lineNumber": 126 }, { "id": "chrislyons_dev_flarelette_jwt__core__getpublicjwkstring", @@ -209,7 +223,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", - "lineNumber": 115 + "lineNumber": 132 }, { "id": "chrislyons_dev_flarelette_jwt__core__getjwksservicename", @@ -221,7 +235,31 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", - "lineNumber": 121 + "lineNumber": 138 + }, + { + "id": "chrislyons_dev_flarelette_jwt__core__getjwksurl", + "componentId": "chrislyons_dev_flarelette_jwt__core", + "name": "getJwksUrl", + "type": "function", + "returnType": "string", + "parameters": [], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", + "lineNumber": 144 + }, + { + "id": "chrislyons_dev_flarelette_jwt__core__getjwkscachettl", + "componentId": "chrislyons_dev_flarelette_jwt__core", + "name": "getJwksCacheTtl", + "type": "function", + "returnType": "number", + "parameters": [], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", + "lineNumber": 148 }, { "description": "Sign a JWT token with explicit configuration", @@ -232,7 +270,7 @@ "documentation": { "summary": "Sign a JWT token with explicit configuration", "examples": [ - "```typescript\n// HS512 mode\nconst config: HS512Config = {\n alg: 'HS512',\n secret: new Uint8Array(32), // Your secret\n iss: 'https://gateway.example.com',\n aud: 'api.example.com',\n ttlSeconds: 900\n}\nconst token = await signWithConfig({ sub: 'user123' }, config)\n\n// EdDSA mode\nconst config: EdDSASignConfig = {\n alg: 'EdDSA',\n privateJwk: { kty: 'OKP', crv: 'Ed25519', d: '...', x: '...' },\n kid: 'ed25519-2025-01',\n iss: 'https://gateway.example.com',\n aud: 'api.example.com'\n}\nconst token = await signWithConfig({ sub: 'user123' }, config)\n```" + "```typescript\r\n// HS512 mode\r\nconst config: HS512Config = {\r\n alg: 'HS512',\r\n secret: new Uint8Array(32), // Your secret\r\n iss: 'https://gateway.example.com',\r\n aud: 'api.example.com',\r\n ttlSeconds: 900\r\n}\r\nconst token = await signWithConfig({ sub: 'user123' }, config)\r\n\r\n// EdDSA mode\r\nconst config: EdDSASignConfig = {\r\n alg: 'EdDSA',\r\n privateJwk: { kty: 'OKP', crv: 'Ed25519', d: '...', x: '...' },\r\n kid: 'ed25519-2025-01',\r\n iss: 'https://gateway.example.com',\r\n aud: 'api.example.com'\r\n}\r\nconst token = await signWithConfig({ sub: 'user123' }, config)\r\n```" ] }, "returnType": "Promise", @@ -260,7 +298,7 @@ "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 102 + "lineNumber": 122 }, { "description": "Verify a JWT token with explicit configuration", @@ -271,7 +309,7 @@ "documentation": { "summary": "Verify a JWT token with explicit configuration", "examples": [ - "```typescript\n// HS512 mode\nconst config: HS512Config = {\n alg: 'HS512',\n secret: new Uint8Array(32), // Same secret used for signing\n iss: 'https://gateway.example.com',\n aud: 'api.example.com'\n}\nconst payload = await verifyWithConfig(token, config)\n\n// EdDSA mode\nconst config: EdDSAVerifyConfig = {\n alg: 'EdDSA',\n publicJwk: { kty: 'OKP', crv: 'Ed25519', x: '...' },\n iss: 'https://gateway.example.com',\n aud: 'api.example.com'\n}\nconst payload = await verifyWithConfig(token, config)\n```" + "```typescript\r\n// HS512 mode\r\nconst config: HS512Config = {\r\n alg: 'HS512',\r\n secret: new Uint8Array(32), // Same secret used for signing\r\n iss: 'https://gateway.example.com',\r\n aud: 'api.example.com'\r\n}\r\nconst payload = await verifyWithConfig(token, config)\r\n\r\n// EdDSA mode\r\nconst config: EdDSAVerifyConfig = {\r\n alg: 'EdDSA',\r\n publicJwk: { kty: 'OKP', crv: 'Ed25519', x: '...' },\r\n iss: 'https://gateway.example.com',\r\n aud: 'api.example.com'\r\n}\r\nconst payload = await verifyWithConfig(token, config)\r\n```" ] }, "returnType": "Promise", @@ -299,16 +337,16 @@ "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 160 + "lineNumber": 181 }, { - "description": "Create a signed JWT token with explicit configuration\n\nHigher-level wrapper around signWithConfig for convenience.", + "description": "Create a signed JWT token with explicit configuration\r\n\r\nHigher-level wrapper around signWithConfig for convenience.", "id": "chrislyons_dev_flarelette_jwt__explicit__createtokenwithconfig", "componentId": "chrislyons_dev_flarelette_jwt__explicit", "name": "createTokenWithConfig", "type": "function", "documentation": { - "summary": "Create a signed JWT token with explicit configuration\n\nHigher-level wrapper around signWithConfig for convenience." + "summary": "Create a signed JWT token with explicit configuration\r\n\r\nHigher-level wrapper around signWithConfig for convenience." }, "returnType": "Promise", "returnDescription": "Signed JWT token string", @@ -335,18 +373,18 @@ "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 209 + "lineNumber": 246 }, { - "description": "Create a delegated JWT token with explicit configuration\n\nImplements RFC 8693 actor claim pattern for service-to-service delegation.", + "description": "Create a delegated JWT token with explicit configuration\r\n\r\nImplements RFC 8693 actor claim pattern for service-to-service delegation.", "id": "chrislyons_dev_flarelette_jwt__explicit__createdelegatedtokenwithconfig", "componentId": "chrislyons_dev_flarelette_jwt__explicit", "name": "createDelegatedTokenWithConfig", "type": "function", "documentation": { - "summary": "Create a delegated JWT token with explicit configuration\n\nImplements RFC 8693 actor claim pattern for service-to-service delegation.", + "summary": "Create a delegated JWT token with explicit configuration\r\n\r\nImplements RFC 8693 actor claim pattern for service-to-service delegation.", "examples": [ - "```typescript\nconst config: HS512Config = {\n alg: 'HS512',\n secret: mySecret,\n iss: 'https://gateway.example.com',\n aud: 'internal-api'\n}\n\n// Gateway receives Auth0 token and creates delegated token\nconst auth0Payload = await verifyAuth0Token(externalToken)\nconst internalToken = await createDelegatedTokenWithConfig(\n auth0Payload,\n 'gateway-service',\n config\n)\n```" + "```typescript\r\nconst config: HS512Config = {\r\n alg: 'HS512',\r\n secret: mySecret,\r\n iss: 'https://gateway.example.com',\r\n aud: 'internal-api'\r\n}\r\n\r\n// Gateway receives Auth0 token and creates delegated token\r\nconst auth0Payload = await verifyAuth0Token(externalToken)\r\nconst internalToken = await createDelegatedTokenWithConfig(\r\n auth0Payload,\r\n 'gateway-service',\r\n config\r\n)\r\n```" ] }, "returnType": "Promise", @@ -380,7 +418,7 @@ "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 246 + "lineNumber": 283 }, { "description": "Verify and authorize a JWT token with explicit configuration", @@ -391,7 +429,7 @@ "documentation": { "summary": "Verify and authorize a JWT token with explicit configuration", "examples": [ - "```typescript\nconst config: HS512Config = {\n alg: 'HS512',\n secret: mySecret,\n iss: 'https://gateway.example.com',\n aud: 'api.example.com'\n}\n\nconst user = await checkAuthWithConfig(token, config, {\n require_all_permissions: ['read:data'],\n require_any_permission: ['admin', 'editor']\n})\n\nif (user) {\n console.log('Authorized user:', user.sub)\n}\n```" + "```typescript\r\nconst config: HS512Config = {\r\n alg: 'HS512',\r\n secret: mySecret,\r\n iss: 'https://gateway.example.com',\r\n aud: 'api.example.com'\r\n}\r\n\r\nconst user = await checkAuthWithConfig(token, config, {\r\n require_all_permissions: ['read:data'],\r\n require_any_permission: ['admin', 'editor']\r\n})\r\n\r\nif (user) {\r\n console.log('Authorized user:', user.sub)\r\n}\r\n```" ] }, "returnType": "Promise", @@ -425,7 +463,7 @@ "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 332 + "lineNumber": 369 }, { "description": "Helper function to create HS512 config from base64url-encoded secret", @@ -455,7 +493,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 386 + "lineNumber": 423 }, { "description": "Helper function to create EdDSA sign config from JWK", @@ -490,7 +528,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 414 + "lineNumber": 452 }, { "description": "Helper function to create EdDSA verify config from JWK", @@ -519,7 +557,46 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", - "lineNumber": 436 + "lineNumber": 474 + }, + { + "description": "Helper function to create HTTP JWKS URL verification config\r\n\r\nEnables testing without environment variables by providing explicit configuration", + "id": "chrislyons_dev_flarelette_jwt__explicit__createjwksurlverifyconfig", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "createJWKSUrlVerifyConfig", + "type": "function", + "documentation": { + "summary": "Helper function to create HTTP JWKS URL verification config\r\n\r\nEnables testing without environment variables by providing explicit configuration", + "examples": [ + "```typescript\r\n// Auth0 configuration\r\nconst config = createJWKSUrlVerifyConfig(\r\n 'https://tenant.auth0.com/.well-known/jwks.json',\r\n {\r\n iss: 'https://tenant.auth0.com/',\r\n aud: 'my-client-id'\r\n }\r\n)\r\n\r\nconst payload = await verifyWithConfig(token, config)\r\n```" + ] + }, + "returnType": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").JWKSUrlVerifyConfig", + "returnDescription": "JWKS URL verification configuration", + "parameters": [ + { + "name": "jwksUrl", + "type": "string", + "description": "- HTTP(S) URL to JWKS endpoint", + "optional": false + }, + { + "name": "baseConfig", + "type": "Omit & Partial>", + "description": "- Base JWT configuration", + "optional": false + }, + { + "name": "cacheTtl", + "type": "number", + "description": "- Optional cache TTL in seconds (default: 300)", + "optional": true + } + ], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 511 }, { "description": "Create a signed JWT token with optional claims", @@ -643,8 +720,8 @@ }, { "description": "Clear the JWKS cache (for testing purposes)", - "id": "chrislyons_dev_flarelette_jwt__util__clearjwkscache", - "componentId": "chrislyons_dev_flarelette_jwt__util", + "id": "chrislyons_dev_flarelette_jwt__jwks__clearjwkscache", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", "name": "clearJwksCache", "type": "function", "documentation": { @@ -655,12 +732,28 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", - "lineNumber": 37 + "lineNumber": 49 + }, + { + "description": "Clear the HTTP JWKS cache (for testing purposes)", + "id": "chrislyons_dev_flarelette_jwt__jwks__clearhttpjwkscache", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", + "name": "clearHttpJwksCache", + "type": "function", + "documentation": { + "summary": "Clear the HTTP JWKS cache (for testing purposes)" + }, + "returnType": "void", + "parameters": [], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", + "lineNumber": 57 }, { "description": "Fetch JWKS from a service binding\nImplements 5-minute caching to reduce load on JWKS service", - "id": "chrislyons_dev_flarelette_jwt__util__fetchjwksfromservice", - "componentId": "chrislyons_dev_flarelette_jwt__util", + "id": "chrislyons_dev_flarelette_jwt__jwks__fetchjwksfromservice", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", "name": "fetchJwksFromService", "type": "function", "documentation": { @@ -677,39 +770,97 @@ "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", - "lineNumber": 45 + "lineNumber": 65 }, { - "description": "Find and import a specific key from JWKS by kid", - "id": "chrislyons_dev_flarelette_jwt__util__getkeyfromjwks", - "componentId": "chrislyons_dev_flarelette_jwt__util", + "description": "Validate JWKS URL for security requirements\n\nRequirements:\n- Must be valid URL format\n- Must use HTTPS (except localhost/127.0.0.1/[::1] for testing)", + "id": "chrislyons_dev_flarelette_jwt__jwks__validatejwksurl", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", + "name": "validateJwksUrl", + "type": "function", + "documentation": { + "summary": "Validate JWKS URL for security requirements\n\nRequirements:\n- Must be valid URL format\n- Must use HTTPS (except localhost/127.0.0.1/[::1] for testing)" + }, + "returnType": "URL", + "returnDescription": "Parsed URL object", + "parameters": [ + { + "name": "url", + "type": "string", + "description": "- JWKS URL to validate", + "optional": false + } + ], + "visibility": "private", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", + "lineNumber": 103 + }, + { + "description": "Fetch JWKS from HTTP URL with caching\n\nImplements configurable TTL caching (default 5 minutes)\nSecurity: HTTPS-only (except localhost), 5-second timeout, 100KB size limit", + "id": "chrislyons_dev_flarelette_jwt__jwks__fetchjwksfromurl", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", + "name": "fetchJwksFromUrl", + "type": "function", + "documentation": { + "summary": "Fetch JWKS from HTTP URL with caching\n\nImplements configurable TTL caching (default 5 minutes)\nSecurity: HTTPS-only (except localhost), 5-second timeout, 100KB size limit" + }, + "returnType": "Promise", + "returnDescription": "Array of JWK objects", + "parameters": [ + { + "name": "url", + "type": "string", + "description": "- HTTP(S) URL to JWKS endpoint", + "optional": false + }, + { + "name": "ttlSeconds", + "type": "number", + "description": "- Cache TTL in seconds (default: 300)", + "optional": true, + "defaultValue": "300" + } + ], + "visibility": "public", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", + "lineNumber": 138 + }, + { + "description": "Find and import a specific key from JWKS by kid\n\nSupports both EdDSA (Ed25519) and RSA (RS256/RS384/RS512) keys\nAlgorithm is auto-detected from key type (kty) and curve (crv)", + "id": "chrislyons_dev_flarelette_jwt__jwks__getkeyfromjwks", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", "name": "getKeyFromJwks", "type": "function", "documentation": { - "summary": "Find and import a specific key from JWKS by kid" + "summary": "Find and import a specific key from JWKS by kid\n\nSupports both EdDSA (Ed25519) and RSA (RS256/RS384/RS512) keys\nAlgorithm is auto-detected from key type (kty) and curve (crv)" }, "returnType": "Promise | CryptoKey>", + "returnDescription": "CryptoKey or Uint8Array suitable for jose verification", "parameters": [ { "name": "kid", "type": "string", + "description": "- Key ID from JWT header", "optional": false }, { "name": "jwks", "type": "JWKWithKid[]", + "description": "- Array of JWK objects", "optional": false } ], "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", - "lineNumber": 75 + "lineNumber": 209 }, { "description": "Get allowed thumbprints for key pinning (optional security measure)", - "id": "chrislyons_dev_flarelette_jwt__util__allowedthumbprints", - "componentId": "chrislyons_dev_flarelette_jwt__util", + "id": "chrislyons_dev_flarelette_jwt__jwks__allowedthumbprints", + "componentId": "chrislyons_dev_flarelette_jwt__jwks", "name": "allowedThumbprints", "type": "function", "documentation": { @@ -720,7 +871,7 @@ "visibility": "public", "isAsync": false, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/jwks.ts", - "lineNumber": 104 + "lineNumber": 242 }, { "id": "chrislyons_dev_flarelette_jwt__util__main", @@ -886,13 +1037,43 @@ "lineNumber": 47 }, { - "description": "Verify a JWT token with HS512 or EdDSA algorithm", - "id": "chrislyons_dev_flarelette_jwt__util__verify", - "componentId": "chrislyons_dev_flarelette_jwt__util", + "description": "Resolve verification key from configured sources\n\nImplements key resolution strategy pattern:\n- Strategy 1: HS512 shared secret\n- Strategy 2: Inline public JWK\n- Strategy 3: Service binding JWKS\n- Strategy 4: HTTP JWKS URL", + "id": "chrislyons_dev_flarelette_jwt__verify__resolveverificationkey", + "componentId": "chrislyons_dev_flarelette_jwt__verify", + "name": "resolveVerificationKey", + "type": "function", + "documentation": { + "summary": "Resolve verification key from configured sources\n\nImplements key resolution strategy pattern:\n- Strategy 1: HS512 shared secret\n- Strategy 2: Inline public JWK\n- Strategy 3: Service binding JWKS\n- Strategy 4: HTTP JWKS URL" + }, + "returnType": "Promise<{ key: Uint8Array | CryptoKey; algorithms: string[]; }>", + "returnDescription": "Key and allowed algorithms", + "parameters": [ + { + "name": "token", + "type": "string", + "description": "- JWT token string", + "optional": false + }, + { + "name": "opts", + "type": "Partial<{ jwksService: import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types\").Fetcher; jwksUrl: string; jwksCacheTtl: number; }>", + "description": "- Verification options", + "optional": true + } + ], + "visibility": "private", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", + "lineNumber": 47 + }, + { + "description": "Verify a JWT token with HS512, EdDSA, or RSA algorithms\n\nSupports multiple key resolution strategies with automatic algorithm detection", + "id": "chrislyons_dev_flarelette_jwt__verify__verify", + "componentId": "chrislyons_dev_flarelette_jwt__verify", "name": "verify", "type": "function", "documentation": { - "summary": "Verify a JWT token with HS512 or EdDSA algorithm" + "summary": "Verify a JWT token with HS512, EdDSA, or RSA algorithms\n\nSupports multiple key resolution strategies with automatic algorithm detection" }, "returnType": "Promise", "returnDescription": "Decoded payload if valid, null otherwise", @@ -905,15 +1086,15 @@ }, { "name": "opts", - "type": "Partial<{ iss: string; aud: string | string[]; leeway: number; jwksService: import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types\").Fetcher; }>", - "description": "- Optional overrides for iss, aud, leeway, and jwksService", + "type": "Partial<{ iss: string; aud: string | string[]; leeway: number; jwksService: import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types\").Fetcher; jwksUrl: string; jwksCacheTtl: number; }>", + "description": "- Optional overrides for iss, aud, leeway, jwksService, jwksUrl, jwksCacheTtl", "optional": true } ], "visibility": "public", "isAsync": true, "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", - "lineNumber": 28 + "lineNumber": 132 }, { "description": "Store both environment variables and service bindings globally", @@ -2906,11 +3087,17 @@ "stereotype": "type-import" }, { - "description": "SignJWT | jwtVerify | importJWK | JWTVerifyResult | JWK", + "description": "SignJWT | jwtVerify | importJWK | decodeProtectedHeader | JWTVerifyResult | JWK", "source": "chrislyons_dev_flarelette_jwt__explicit", "destination": "jose", "stereotype": "import" }, + { + "description": "fetchJwksFromUrl | getKeyFromJwks", + "source": "chrislyons_dev_flarelette_jwt__explicit", + "destination": "./jwks.js", + "stereotype": "import" + }, { "description": "imports JwtPayload", "source": "chrislyons_dev_flarelette_jwt__explicit", @@ -2930,13 +3117,25 @@ "stereotype": "import" }, { - "description": "Fetcher | JwtPayload | JWKSResponse", + "description": "Fetcher | JwtPayload", "source": "chrislyons_dev_flarelette_jwt__util", "destination": "./types.js", "stereotype": "type-import" }, { - "description": "importJWK | generateKeyPair | exportJWK | jwtVerify | calculateJwkThumbprint | decodeProtectedHeader", + "description": "imports importJWK", + "source": "chrislyons_dev_flarelette_jwt__jwks", + "destination": "jose", + "stereotype": "import" + }, + { + "description": "Fetcher | JWKSResponse", + "source": "chrislyons_dev_flarelette_jwt__jwks", + "destination": "./types.js", + "stereotype": "type-import" + }, + { + "description": "generateKeyPair | exportJWK", "source": "chrislyons_dev_flarelette_jwt__util", "destination": "jose", "stereotype": "import" @@ -2954,17 +3153,35 @@ "stereotype": "import" }, { - "description": "ParsedJwt | JwtPayload | AlgType | Fetcher", + "description": "ParsedJwt | JwtPayload", "source": "chrislyons_dev_flarelette_jwt__util", "destination": "chrislyons_dev_flarelette_jwt__types", "stereotype": "type-import" }, { - "description": "envMode | getCommon | getHSSecret | getPublicJwkString", - "source": "chrislyons_dev_flarelette_jwt__util", + "description": "jwtVerify | importJWK | calculateJwkThumbprint | decodeProtectedHeader", + "source": "chrislyons_dev_flarelette_jwt__verify", + "destination": "jose", + "stereotype": "import" + }, + { + "description": "envMode | getCommon | getHSSecret | getPublicJwkString | getJwksUrl | getJwksCacheTtl", + "source": "chrislyons_dev_flarelette_jwt__verify", "destination": "chrislyons_dev_flarelette_jwt__core", "stereotype": "import" }, + { + "description": "fetchJwksFromService | fetchJwksFromUrl | getKeyFromJwks | allowedThumbprints", + "source": "chrislyons_dev_flarelette_jwt__verify", + "destination": "chrislyons_dev_flarelette_jwt__jwks", + "stereotype": "import" + }, + { + "description": "AlgType | Fetcher | JwtPayload", + "source": "chrislyons_dev_flarelette_jwt__verify", + "destination": "chrislyons_dev_flarelette_jwt__types", + "stereotype": "type-import" + }, { "description": "imports * as kit", "source": "chrislyons_dev_flarelette_jwt__adapters", @@ -3141,6 +3358,12 @@ "destination": "jose:importJWK", "stereotype": "import" }, + { + "description": "imports decodeProtectedHeader", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "jose:decodeProtectedHeader", + "stereotype": "import" + }, { "description": "imports JWTVerifyResult", "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", @@ -3153,6 +3376,18 @@ "destination": "jose:JWK", "stereotype": "import" }, + { + "description": "imports fetchJwksFromUrl", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "./jwks.js:fetchJwksFromUrl", + "stereotype": "import" + }, + { + "description": "imports getKeyFromJwks", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "./jwks.js:getKeyFromJwks", + "stereotype": "import" + }, { "description": "imports JwtPayload", "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", @@ -3327,12 +3562,30 @@ "destination": "./config.js:getPublicJwkString", "stereotype": "import" }, + { + "description": "imports getJwksUrl", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", + "destination": "./config.js:getJwksUrl", + "stereotype": "import" + }, + { + "description": "imports getJwksCacheTtl", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", + "destination": "./config.js:getJwksCacheTtl", + "stereotype": "import" + }, { "description": "imports fetchJwksFromService", "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", "destination": "./jwks.js:fetchJwksFromService", "stereotype": "import" }, + { + "description": "imports fetchJwksFromUrl", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", + "destination": "./jwks.js:fetchJwksFromUrl", + "stereotype": "import" + }, { "description": "imports getKeyFromJwks", "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/verify.ts", diff --git a/docs/architecture/flarelette-jwt-kit.dsl b/docs/architecture/flarelette-jwt-kit.dsl index 1cda232..5b5d1f4 100644 --- a/docs/architecture/flarelette-jwt-kit.dsl +++ b/docs/architecture/flarelette-jwt-kit.dsl @@ -24,17 +24,25 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "module" } chrislyons_dev_flarelette_jwt__util = component "util" { - description "High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | JSON Web Key Set (JWKS) utilities. This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). It supports integration with external JWKS services. | Key generation utility for EdDSA keys. This script generates EdDSA key pairs and exports them in JWK format. It is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities. This module provides functions to generate secure secrets and validate base64url-encoded secrets. It ensures compatibility with JWT signing requirements. | Utility functions for JWT operations. This module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes. It is designed to support core JWT functionalities. | JWT verification utilities. This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. It supports integration with JWKS services and thumbprint pinning." + description "High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | Key generation utility for EdDSA keys. This script generates EdDSA key pairs and exports them in JWK format. It is designed to be executed as a standalone Node.js script. | Secret generation and validation utilities. This module provides functions to generate secure secrets and validate base64url-encoded secrets. It ensures compatibility with JWT signing requirements. | Utility functions for JWT operations. This module provides helper functions for parsing JWTs, checking expiration, and mapping OAuth scopes. It is designed to support core JWT functionalities." technology "module" } chrislyons_dev_flarelette_jwt__main = component "main" { description "Entry point for the flarelette-jwt library. This module re-exports core functionalities, including signing, verification, utilities, and type definitions. It serves as the main interface for library consumers." technology "module" } + chrislyons_dev_flarelette_jwt__jwks = component "jwks" { + description "JSON Web Key Set (JWKS) utilities. This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). It supports integration with external JWKS services." + technology "module" + } chrislyons_dev_flarelette_jwt__types = component "types" { description "Type definitions for JWT operations. This module defines types for JWT headers, payloads, profiles, and related structures. It ensures type safety and consistency across the library." technology "module" } + chrislyons_dev_flarelette_jwt__verify = component "verify" { + description "JWT verification utilities. This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. It supports integration with JWKS services and thumbprint pinning." + technology "module" + } chrislyons_dev_flarelette_jwt__adapters = component "adapters" { description "Component inferred from directory: adapters" technology "module" @@ -75,6 +83,14 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "function" tags "Code" } + chrislyons_dev_flarelette_jwt__core__getjwksurl = component "core.getJwksUrl" { + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__core__getjwkscachettl = component "core.getJwksCacheTtl" { + technology "function" + tags "Code" + } chrislyons_dev_flarelette_jwt__explicit__signwithconfig = component "explicit.signWithConfig" { description "Sign a JWT token with explicit configuration" technology "function" @@ -115,6 +131,11 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "function" tags "Code" } + chrislyons_dev_flarelette_jwt__explicit__createjwksurlverifyconfig = component "explicit.createJWKSUrlVerifyConfig" { + description "Helper function to create HTTP JWKS URL verification config Enables testing without environment variables by providing explicit configuration" + technology "function" + tags "Code" + } chrislyons_dev_flarelette_jwt__util__createtoken = component "util.createToken" { description "Create a signed JWT token with optional claims" technology "function" @@ -135,22 +156,37 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "function" tags "Code" } - chrislyons_dev_flarelette_jwt__util__clearjwkscache = component "util.clearJwksCache" { + chrislyons_dev_flarelette_jwt__jwks__clearjwkscache = component "jwks.clearJwksCache" { description "Clear the JWKS cache (for testing purposes)" technology "function" tags "Code" } - chrislyons_dev_flarelette_jwt__util__fetchjwksfromservice = component "util.fetchJwksFromService" { + chrislyons_dev_flarelette_jwt__jwks__clearhttpjwkscache = component "jwks.clearHttpJwksCache" { + description "Clear the HTTP JWKS cache (for testing purposes)" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__jwks__fetchjwksfromservice = component "jwks.fetchJwksFromService" { description "Fetch JWKS from a service binding Implements 5-minute caching to reduce load on JWKS service" technology "function" tags "Code" } - chrislyons_dev_flarelette_jwt__util__getkeyfromjwks = component "util.getKeyFromJwks" { - description "Find and import a specific key from JWKS by kid" + chrislyons_dev_flarelette_jwt__jwks__validatejwksurl = component "jwks.validateJwksUrl" { + description "Validate JWKS URL for security requirements Requirements: - Must be valid URL format - Must use HTTPS (except localhost/127.0.0.1/[::1] for testing)" technology "function" tags "Code" } - chrislyons_dev_flarelette_jwt__util__allowedthumbprints = component "util.allowedThumbprints" { + chrislyons_dev_flarelette_jwt__jwks__fetchjwksfromurl = component "jwks.fetchJwksFromUrl" { + description "Fetch JWKS from HTTP URL with caching Implements configurable TTL caching (default 5 minutes) Security: HTTPS-only (except localhost), 5-second timeout, 100KB size limit" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__jwks__getkeyfromjwks = component "jwks.getKeyFromJwks" { + description "Find and import a specific key from JWKS by kid Supports both EdDSA (Ed25519) and RSA (RS256/RS384/RS512) keys Algorithm is auto-detected from key type (kty) and curve (crv)" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__jwks__allowedthumbprints = component "jwks.allowedThumbprints" { description "Get allowed thumbprints for key pinning (optional security measure)" technology "function" tags "Code" @@ -187,8 +223,13 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "function" tags "Code" } - chrislyons_dev_flarelette_jwt__util__verify = component "util.verify" { - description "Verify a JWT token with HS512 or EdDSA algorithm" + chrislyons_dev_flarelette_jwt__verify__resolveverificationkey = component "verify.resolveVerificationKey" { + description "Resolve verification key from configured sources Implements key resolution strategy pattern: - Strategy 1: HS512 shared secret - Strategy 2: Inline public JWK - Strategy 3: Service binding JWKS - Strategy 4: HTTP JWKS URL" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__verify__verify = component "verify.verify" { + description "Verify a JWT token with HS512, EdDSA, or RSA algorithms Supports multiple key resolution strategies with automatic algorithm detection" technology "function" tags "Code" } @@ -209,8 +250,10 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { } # Component relationships - chrislyons_dev_flarelette_jwt__util -> chrislyons_dev_flarelette_jwt__types "ParsedJwt | JwtPayload | AlgType | Fetcher" - chrislyons_dev_flarelette_jwt__util -> chrislyons_dev_flarelette_jwt__core "envMode | getCommon | getHSSecret | getPublicJwkString" + chrislyons_dev_flarelette_jwt__util -> chrislyons_dev_flarelette_jwt__types "ParsedJwt | JwtPayload" + chrislyons_dev_flarelette_jwt__verify -> chrislyons_dev_flarelette_jwt__core "envMode | getCommon | getHSSecret | getPublicJwkString | getJwksUrl | getJwksCacheTtl" + chrislyons_dev_flarelette_jwt__verify -> chrislyons_dev_flarelette_jwt__jwks "fetchJwksFromService | fetchJwksFromUrl | getKeyFromJwks | allowedThumbprints" + chrislyons_dev_flarelette_jwt__verify -> chrislyons_dev_flarelette_jwt__types "AlgType | Fetcher | JwtPayload" chrislyons_dev_flarelette_jwt__adapters -> chrislyons_dev_flarelette_jwt__main "imports * as kit" chrislyons_dev_flarelette_jwt__adapters -> chrislyons_dev_flarelette_jwt__core "imports getJwksServiceName" chrislyons_dev_flarelette_jwt__adapters -> chrislyons_dev_flarelette_jwt__types "WorkerEnv | Fetcher" @@ -746,7 +789,9 @@ branding { include chrislyons_dev_flarelette_jwt__explicit include chrislyons_dev_flarelette_jwt__util include chrislyons_dev_flarelette_jwt__main + include chrislyons_dev_flarelette_jwt__jwks include chrislyons_dev_flarelette_jwt__types + include chrislyons_dev_flarelette_jwt__verify include chrislyons_dev_flarelette_jwt__adapters exclude "element.tag==Code" autoLayout @@ -772,6 +817,8 @@ branding { include chrislyons_dev_flarelette_jwt__core__getprivatejwkstring include chrislyons_dev_flarelette_jwt__core__getpublicjwkstring include chrislyons_dev_flarelette_jwt__core__getjwksservicename + include chrislyons_dev_flarelette_jwt__core__getjwksurl + include chrislyons_dev_flarelette_jwt__core__getjwkscachettl include chrislyons_dev_flarelette_jwt__core__sign autoLayout } @@ -786,6 +833,7 @@ branding { include chrislyons_dev_flarelette_jwt__explicit__createhs512config include chrislyons_dev_flarelette_jwt__explicit__createeddsasignconfig include chrislyons_dev_flarelette_jwt__explicit__createeddsaverifyconfig + include chrislyons_dev_flarelette_jwt__explicit__createjwksurlverifyconfig autoLayout } @@ -795,17 +843,31 @@ branding { include chrislyons_dev_flarelette_jwt__util__createdelegatedtoken include chrislyons_dev_flarelette_jwt__util__checkauth include chrislyons_dev_flarelette_jwt__util__policy - include chrislyons_dev_flarelette_jwt__util__clearjwkscache - include chrislyons_dev_flarelette_jwt__util__fetchjwksfromservice - include chrislyons_dev_flarelette_jwt__util__getkeyfromjwks - include chrislyons_dev_flarelette_jwt__util__allowedthumbprints include chrislyons_dev_flarelette_jwt__util__main include chrislyons_dev_flarelette_jwt__util__generatesecret include chrislyons_dev_flarelette_jwt__util__isvalidbase64urlsecret include chrislyons_dev_flarelette_jwt__util__parse include chrislyons_dev_flarelette_jwt__util__isexpiringsoon include chrislyons_dev_flarelette_jwt__util__mapscopestopermissions - include chrislyons_dev_flarelette_jwt__util__verify + autoLayout + } + + + component chrislyons_dev_flarelette_jwt "Classes_chrislyons_dev_flarelette_jwt__jwks" { + include chrislyons_dev_flarelette_jwt__jwks__clearjwkscache + include chrislyons_dev_flarelette_jwt__jwks__clearhttpjwkscache + include chrislyons_dev_flarelette_jwt__jwks__fetchjwksfromservice + include chrislyons_dev_flarelette_jwt__jwks__validatejwksurl + include chrislyons_dev_flarelette_jwt__jwks__fetchjwksfromurl + include chrislyons_dev_flarelette_jwt__jwks__getkeyfromjwks + include chrislyons_dev_flarelette_jwt__jwks__allowedthumbprints + autoLayout + } + + + component chrislyons_dev_flarelette_jwt "Classes_chrislyons_dev_flarelette_jwt__verify" { + include chrislyons_dev_flarelette_jwt__verify__resolveverificationkey + include chrislyons_dev_flarelette_jwt__verify__verify autoLayout } diff --git a/docs/core-concepts.md b/docs/core-concepts.md index d86365b..450f1d9 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -4,7 +4,11 @@ Understanding how Flarelette JWT Kit makes cryptographic and architectural decis ## Algorithm Selection -The kit supports exactly two JWT algorithms by design. No configuration required — the mode is detected automatically from your environment. +The kit supports **two signing algorithms** (HS512, EdDSA) and **three verification profiles** (HS512, EdDSA, RSA). No configuration required — the mode is detected automatically from your environment. + +**Signing:** HS512 for symmetric trust, EdDSA for asymmetric trust. + +**Verification:** HS512 and EdDSA for internal tokens, RSA for external OIDC providers. ### HS512 (Symmetric) @@ -64,9 +68,44 @@ JWT_PUBLIC_JWK_NAME=GATEWAY_PUBLIC # Option 2: Service binding for JWKS (supports rotation) JWT_JWKS_SERVICE_NAME=GATEWAY_BINDING + +# Option 3: HTTP JWKS URL for external OIDC providers (TypeScript only) +JWT_JWKS_URL=https://tenant.auth0.com/.well-known/jwks.json +``` + +### RSA (External OIDC Verification) + +**RS256, RS384, RS512** verification for external OIDC providers. + +**Use when:** + +- Verifying tokens from Auth0, Okta, Google, Azure AD, or Cloudflare Access +- Gateway integrates with external identity providers +- Tokens are signed externally, only verification is needed + +**Security properties:** + +- Verification-only (no signing capability) +- Supports key rotation via JWKS +- HTTPS-only URL fetching with caching +- TypeScript only (Python pending Cloudflare runtime improvements) + +**Environment detection (consumer):** + +```bash +JWT_JWKS_URL=https://tenant.auth0.com/.well-known/jwks.json +JWT_JWKS_CACHE_TTL_SECONDS=300 # Optional: default 5 minutes ``` -### Why Only Two Algorithms? +**Supported OIDC providers:** + +- **Auth0**: `https://tenant.auth0.com/.well-known/jwks.json` +- **Okta**: `https://domain.okta.com/oauth2/default/v1/keys` +- **Google**: `https://www.googleapis.com/oauth2/v3/certs` +- **Azure AD**: `https://login.microsoftonline.com/tenant-id/discovery/v2.0/keys` +- **Cloudflare Access**: `https://team.cloudflareaccess.com/cdn-cgi/access/certs` + +### Algorithm Design Philosophy **Reduced attack surface:** Fewer algorithms means less code to audit and fewer potential vulnerabilities. @@ -86,10 +125,12 @@ Producer (signing): Otherwise → HS512 mode Consumer (verification): - If JWT_PUBLIC_JWK* or JWT_JWKS_SERVICE* exists → EdDSA mode + If JWT_PUBLIC_JWK* or JWT_JWKS_SERVICE* or JWT_JWKS_URL exists → EdDSA/RSA mode Otherwise → HS512 mode ``` +**Note:** EdDSA/RSA mode supports both EdDSA (Ed25519) and RSA (RS256/384/512) verification. The actual algorithm is auto-detected from the JWK structure or token header. + **Verification in code:** **TypeScript:** @@ -110,6 +151,105 @@ detected = mode('producer') # or 'consumer' print(f'Detected mode: {detected}') # "HS512" or "EdDSA" ``` +## JWKS Resolution Strategies + +When verifying EdDSA or RSA tokens, the kit supports four strategies for obtaining the verification key. Each strategy has different trade-offs for security, performance, and operational complexity. + +### Strategy 1: HS512 Shared Secret + +**Use case:** Simplest configuration for trusted producer-consumer pairs. + +**Configuration:** + +```bash +JWT_SECRET_NAME=MY_JWT_SECRET +``` + +**Characteristics:** + +- Single secret shared between producer and consumer +- No key distribution complexity +- Fastest verification (symmetric) +- Requires mutual trust between services + +**When to use:** Internal services where both producer and consumer are under your control. + +### Strategy 2: Inline Public JWK + +**Use case:** Single EdDSA verification key, no rotation needed. + +**Configuration:** + +```bash +JWT_PUBLIC_JWK_NAME=GATEWAY_PUBLIC +``` + +**Characteristics:** + +- Public key embedded in environment +- No network calls during verification +- No key rotation support (requires redeployment to update key) +- Works in both TypeScript and Python + +**When to use:** Internal services with infrequent key rotation, or Python Workers verifying EdDSA tokens. + +### Strategy 3: Service Binding JWKS + +**Use case:** Internal key rotation with Worker-to-Worker RPC. + +**Configuration:** + +```toml +# Consumer wrangler.toml +[[services]] +binding = "GATEWAY_BINDING" +service = "jwt-gateway" + +[vars] +JWT_JWKS_SERVICE_NAME = "GATEWAY_BINDING" +``` + +**Characteristics:** + +- Keys fetched from internal JWKS endpoint via Worker-to-Worker RPC +- Supports multiple active keys (key rotation) +- No public HTTP endpoint required +- Cached for 5 minutes +- TypeScript only + +**When to use:** Internal service mesh with key rotation requirements and no external OIDC provider. + +### Strategy 4: HTTP JWKS URL + +**Use case:** External OIDC provider verification (Auth0, Okta, Google, Azure AD). + +**Configuration:** + +```bash +JWT_JWKS_URL=https://tenant.auth0.com/.well-known/jwks.json +JWT_JWKS_CACHE_TTL_SECONDS=300 # Optional: default 5 minutes +``` + +**Characteristics:** + +- Keys fetched from public HTTPS endpoint +- Supports multiple active keys (key rotation) +- HTTPS-only (except localhost for testing) +- Cached with configurable TTL +- TypeScript only +- 5-second timeout, 100KB size limit + +**When to use:** Verifying tokens from external OIDC providers like Auth0, Okta, Google Workspace, Azure AD, or Cloudflare Access. + +### Comparison Table + +| Strategy | Key Rotation | Network Calls | Python Support | Use Case | +| ----------------- | ------------ | ------------- | -------------- | --------------------------- | +| HS512 Shared | ❌ | ❌ | ✅ | Trusted internal services | +| Inline Public JWK | ❌ | ❌ | ✅ | Single key, no rotation | +| Service Binding | ✅ | ✅ (internal) | ❌ | Internal mesh with rotation | +| HTTP JWKS URL | ✅ | ✅ (external) | ❌ | External OIDC providers | + ## Secret-Name Indirection Instead of storing secrets directly in environment variables, reference the binding name. This enables proper secret management in Cloudflare Workers. @@ -285,6 +425,7 @@ TypeScript and Python implementations are kept in sync: | HS512 verification | ✅ | ✅ | | EdDSA signing | ✅ | ❌ (use Node gateway) | | EdDSA verification | ✅ | ✅ (inline JWK only) | +| RSA verification | ✅ | ❌ | | JWKS fetch | ✅ | ❌ (inline JWK only) | | Service bindings | ✅ | ❌ | | Secret-name indirection | ✅ | ✅ | diff --git a/docs/explicit-config.md b/docs/explicit-config.md index 5a6fe90..d4a639b 100644 --- a/docs/explicit-config.md +++ b/docs/explicit-config.md @@ -110,7 +110,7 @@ Symmetric (shared secret) configuration: ```typescript interface HS512Config extends BaseJwtConfig { alg: 'HS512' - secret: Uint8Array // Minimum 32 bytes + secret: Uint8Array // Minimum 64 bytes (HS512 requirement) } ``` @@ -256,10 +256,10 @@ Create EdDSA verification configuration from public JWK. **Solution:** Create config objects directly: ```typescript -// No .env files needed! +// No .env files needed const devConfig = { alg: 'HS512' as const, - secret: new Uint8Array(32), // Simple dev secret + secret: new Uint8Array(64), // 64-byte dev secret iss: 'http://localhost:3000', aud: ['http://localhost:3001', 'http://localhost:3002'], ttlSeconds: 3600, // 1 hour @@ -279,7 +279,7 @@ const token = await createTokenWithConfig({ sub: 'dev-user' }, devConfig) describe('JWT authentication', () => { const testConfig = { alg: 'HS512' as const, - secret: new Uint8Array(32), + secret: new Uint8Array(64), iss: 'test-issuer', aud: 'test-audience', } @@ -341,7 +341,7 @@ const payload = await verify(token) ### Explicit Configuration API (New) ```typescript -// No environment variables required! +// No environment variables required import { signWithConfig, verifyWithConfig, @@ -439,7 +439,7 @@ function createJwtConfig(env: 'dev' | 'prod'): HS512Config { if (env === 'dev') { return { alg: 'HS512', - secret: new Uint8Array(32), // Dev secret + secret: new Uint8Array(64), // 64-byte dev secret iss: 'http://localhost:3000', aud: 'http://localhost:3001', ttlSeconds: 3600, diff --git a/docs/getting-started.md b/docs/getting-started.md index 9d290d2..e557078 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -204,6 +204,49 @@ else: print("Authorization failed") ``` +## Explicit Configuration (No Environment Variables) + +> **New in v1.9.0**: Pass configuration directly without environment setup + +For development, testing, or scenarios where environment variables are inconvenient, use the explicit configuration API: + +**TypeScript:** + +```typescript +import { + createHS512Config, + createTokenWithConfig, + verifyWithConfig, +} from '@chrislyons-dev/flarelette-jwt' + +// Create config object (no environment variables needed) +const config = createHS512Config('your-base64url-secret-here', { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + ttlSeconds: 900, +}) + +// Sign and verify +const token = await createTokenWithConfig( + { + sub: 'user123', + permissions: ['read:data'], + }, + config +) + +const payload = await verifyWithConfig(token, config) +console.log('User:', payload?.sub) +``` + +**When to use:** + +- Development environments (skip .env setup) +- Testing (isolated configs per test) +- Multi-tenant apps (different configs per tenant) + +**Full documentation:** [Explicit Configuration API](./explicit-config.md) + ## Next Steps - **[Core Concepts](./core-concepts.md)** — Understand algorithms, modes, and architecture diff --git a/docs/quick-start-demo.md b/docs/quick-start-demo.md index 6ca49b6..2a6b5a5 100644 --- a/docs/quick-start-demo.md +++ b/docs/quick-start-demo.md @@ -118,7 +118,7 @@ app.use('*', async (c, next) => { ## Development Environment Setup -### No .dev.vars Required! +### No .dev.vars Required Create a shared config for all services: diff --git a/docs/security-guide.md b/docs/security-guide.md index 59477da..7ea96b8 100644 --- a/docs/security-guide.md +++ b/docs/security-guide.md @@ -1,10 +1,152 @@ # Security Guide -Comprehensive security baseline for Flarelette JWT Kit across HS512 and EdDSA profiles. +Comprehensive security baseline for Flarelette JWT Kit across HS512, EdDSA, and RSA profiles. + +## Trust Model: Why This Library Is Secure + +Flarelette JWT Kit is designed from the ground up to prevent the most common JWT vulnerabilities. This section explains **exactly how** we mitigate historic attacks. + +### Protection Against Historic JWT Vulnerabilities + +#### 1. Algorithm Confusion Attacks (CVE-2015-2951: `alg: none`) + +**Vulnerability:** Attacker sets `alg: "none"` in token header, library accepts unsigned tokens. + +**Our Protection:** + +- **Mode determined by server configuration only** — Verification mode (HS512 vs EdDSA/RSA) is chosen exclusively from server environment variables, never from the token header +- **Strict algorithm whitelists** — Each mode has an explicit whitelist of allowed algorithms: + - HS512 mode: `['HS512']` only + - EdDSA/RSA mode: `['EdDSA', 'RS256', 'RS384', 'RS512']` only +- **No `none` algorithm support** — The `none` algorithm is never included in any whitelist +- **Token `alg` treated as untrusted input** — The `alg` header must match the allowed algorithms for the selected mode. Mismatches are rejected. + +**Code location:** `src/verify.ts:145-152` (verification with explicit algorithm whitelist) + +#### 2. Algorithm Substitution (CVE-2015-9235: RS256 Public Key as HMAC Secret) + +**Vulnerability:** Attacker obtains RSA public key, creates HMAC-signed token, library verifies using public key as HMAC secret. + +**Our Protection:** + +- **Symmetric and asymmetric keys never shared** — HS512 and EdDSA/RSA use completely separate code paths +- **Configuration conflict detection** — Throws error if both `JWT_SECRET` (HS512) and `JWT_PUBLIC_JWK`/`JWT_JWKS_*` (asymmetric) are configured +- **Separate verification strategies** — Key resolution uses strategy pattern with no code path allowing symmetric key to be used for asymmetric verification + +**Code location:** `src/config.ts:36-51` (mode conflict detection) + +```typescript +// SECURITY: Detect conflicting configuration +if (hasHS512 && hasAsymmetric) { + throw new Error( + 'Configuration error: Both HS512 (JWT_SECRET) and asymmetric (JWT_PUBLIC_JWK/JWT_JWKS_*) secrets configured. Choose one to prevent algorithm confusion attacks.' + ) +} +``` + +#### 3. JWKS Injection Attacks + +**Vulnerability:** Token includes `jku` (JWKS URL) or `x5u` (X.509 URL) header, attacker points to malicious key server. + +**Our Protection:** + +- **JWKS URL pinned in server configuration** — `JWT_JWKS_URL` is set in environment variables, never read from token headers +- **No `jku`/`x5u` header support** — These headers are completely ignored by the library +- **Service binding JWKS** — For Cloudflare Workers, JWKS is fetched via direct Worker-to-Worker RPC (no external URLs) + +**Code location:** `src/verify.ts:102-114` (HTTP JWKS with config-only URL) + +#### 4. Key ID (`kid`) Injection Attacks + +**Vulnerability:** Attacker manipulates `kid` header to perform SQL injection, path traversal, or SSRF. + +**Our Protection:** + +- **`kid` treated as pure lookup key** — Used only for array/map lookups, never interpolated into SQL, file paths, or URLs +- **JWKS array searched by equality** — `kid` compared using strict equality (`===`), no string concatenation or interpolation + +**Code location:** `src/jwks.ts:219` (kid lookup with strict equality) + +```typescript +const jwk = jwks.find(k => k.kid === kid) // Safe: no interpolation +``` + +#### 5. Weak HS512 Secrets + +**Vulnerability:** Short HMAC secrets vulnerable to brute force attacks. + +**Our Protection:** + +- **64-byte minimum enforced** — HS512 requires exactly 64 bytes (512 bits), matching SHA-512 digest size +- **Fail-fast on short secrets** — Configuration with secrets < 64 bytes throws explicit error with remediation instructions +- **CLI tool for secure generation** — `npx flarelette-jwt-secret --len=64` generates cryptographically random secrets + +**Code location:** `src/config.ts:104-109`, `src/explicit.ts:139-142` + +```typescript +// SECURITY: HS512 requires 64-byte minimum (SHA-512 digest size) +if (buf.length < 64) { + throw new Error( + `JWT secret too short: ${buf.length} bytes, need >= 64 for HS512 (use 'npx flarelette-jwt-secret --len=64')` + ) +} +``` + +#### 6. Mode Confusion Within Library + +**Vulnerability:** Library allows both HS512 and asymmetric configuration simultaneously, creating unpredictable behavior. + +**Our Protection:** + +- **Single-mode enforcement** — Configuration error thrown if both symmetric and asymmetric secrets detected +- **Explicit mode detection** — Mode determined once at startup based on environment variables + +**Code location:** `src/config.ts:47-51` + +### Algorithm Pinning at Key Import + +When importing JWKs, the expected algorithm is provided explicitly to the `jose` library: + +```typescript +// Inline JWK import with explicit algorithm +const key = await importJWK(jwk, 'EdDSA') // Algorithm pinned at import time +``` + +This ensures keys cannot be repurposed for other algorithms, even within the same key family (e.g., cannot use Ed25519 key for RS256). + +**Code location:** `src/verify.ts:73` + +### Fail-Silent Pattern with Observability + +**Pattern:** All verification failures return `null` to callers (never throw exceptions). + +**Rationale:** + +- Simplifies error handling in HTTP request handlers +- Prevents information leakage via error messages +- Consistent interface for all failure modes + +**Observability:** While the library returns `null` for all failures, **applications should log verification failures** with structured metadata: + +```typescript +const payload = await verify(token) +if (!payload) { + // Log failure with context (but not the token itself) + console.warn({ + event: 'jwt_verification_failed', + iss: config.iss, // Expected issuer + aud: config.aud, // Expected audience + // DO NOT log the actual token + }) + return new Response('Unauthorized', { status: 401 }) +} +``` + +**Recommendation:** Track verification failure rates in metrics/APM for anomaly detection. ## Cryptographic Profiles -The kit supports exactly two JWT algorithms by design. Each has specific security properties and use cases. +The kit supports three JWT algorithm profiles by design. Each has specific security properties and use cases. ### HS512 (Symmetric) @@ -574,6 +716,7 @@ Before deploying to production: - [ ] HS512 or EdDSA explicitly enforced (not both in same environment) - [ ] Secrets stored as Cloudflare bindings (`*_NAME` pattern) - [ ] TTL ≤ 15 minutes; leeway ≤ 90 seconds +- [ ] `JWT_AUD` is specific per service (no wildcard audiences) — prevents token reuse between services - [ ] No tokens in logs, URLs, or version control - [ ] Minimal claims principle applied (no PII unless necessary) - [ ] Rotation policy documented and tested (both HS512 and EdDSA) diff --git a/packages/flarelette-jwt-ts/package.json b/packages/flarelette-jwt-ts/package.json index 9f6c8ee..7a63162 100644 --- a/packages/flarelette-jwt-ts/package.json +++ b/packages/flarelette-jwt-ts/package.json @@ -1,6 +1,6 @@ { "name": "@chrislyons-dev/flarelette-jwt", - "version": "1.12.0", + "version": "1.13.0", "type": "module", "description": "Environment-driven JWT authentication for Cloudflare Workers with secret-name indirection", "keywords": [ diff --git a/packages/flarelette-jwt-ts/src/config.ts b/packages/flarelette-jwt-ts/src/config.ts index c8cbdb3..41505f1 100644 --- a/packages/flarelette-jwt-ts/src/config.ts +++ b/packages/flarelette-jwt-ts/src/config.ts @@ -34,12 +34,23 @@ export function envMode(role: 'producer' | 'consumer'): AlgType { // Consumers use public keys or JWKS to verify if (role === 'consumer') { - if ( + // SECURITY: Detect conflicting configuration to prevent mode confusion attacks + const hasHS512 = !!(env.JWT_SECRET || env.JWT_SECRET_NAME) + const hasAsymmetric = !!( env.JWT_PUBLIC_JWK || env.JWT_PUBLIC_JWK_NAME || env.JWT_JWKS_SERVICE || - env.JWT_JWKS_SERVICE_NAME - ) { + env.JWT_JWKS_SERVICE_NAME || + env.JWT_JWKS_URL + ) + + if (hasHS512 && hasAsymmetric) { + throw new Error( + 'Configuration error: Both HS512 (JWT_SECRET) and asymmetric (JWT_PUBLIC_JWK/JWT_JWKS_*) secrets configured. Choose one to prevent algorithm confusion attacks.' + ) + } + + if (hasAsymmetric) { return 'EdDSA' } } @@ -90,8 +101,12 @@ export function getHSSecret(): Uint8Array { const b64 = s.replace(/-/g, '+').replace(/_/g, '/') try { const buf = Buffer.from(b64, 'base64') - if (buf.length >= 32) return new Uint8Array(buf) - throw new Error(`JWT secret too short: ${buf.length} bytes, need >= 32`) + // SECURITY: HS512 uses SHA-512 (512 bits = 64 bytes). Enforce minimum 64-byte secret. + // This prevents brute-force attacks and ensures secret entropy matches algorithm strength. + if (buf.length >= 64) return new Uint8Array(buf) + throw new Error( + `JWT secret too short: ${buf.length} bytes, need >= 64 for HS512 (use 'npx flarelette-jwt-secret --len=64')` + ) } catch (e) { if (e instanceof Error && e.message.includes('too short')) throw e // Fallback to UTF-8 encoding for backwards compatibility @@ -99,8 +114,10 @@ export function getHSSecret(): Uint8Array { 'JWT_SECRET is not valid base64url. Treating as raw UTF-8 string (not recommended for production)' ) const bytes = new TextEncoder().encode(s) - if (bytes.length < 32) { - throw new Error(`JWT secret too short: ${bytes.length} bytes, need >= 32`) + if (bytes.length < 64) { + throw new Error( + `JWT secret too short: ${bytes.length} bytes, need >= 64 for HS512 (use 'npx flarelette-jwt-secret --len=64')` + ) } return bytes } @@ -123,3 +140,19 @@ export function getJwksServiceName(): string | null { if (name && envRead(name)) return envRead(name)! return envRead('JWT_JWKS_SERVICE') || null } + +export function getJwksUrl(): string | null { + return envRead('JWT_JWKS_URL') || null +} + +export function getJwksCacheTtl(): number { + const ttl = envRead('JWT_JWKS_CACHE_TTL_SECONDS') + if (!ttl) return 300 // Default: 5 minutes + + const parsed = Number(ttl) + if (isNaN(parsed) || parsed < 0) { + throw new Error('JWT_JWKS_CACHE_TTL_SECONDS must be a positive number') + } + + return parsed +} diff --git a/packages/flarelette-jwt-ts/src/explicit.ts b/packages/flarelette-jwt-ts/src/explicit.ts index 45d5ba1..8f59e8c 100644 --- a/packages/flarelette-jwt-ts/src/explicit.ts +++ b/packages/flarelette-jwt-ts/src/explicit.ts @@ -9,7 +9,15 @@ * @module explicit */ -import { SignJWT, jwtVerify, importJWK, type JWTVerifyResult, type JWK } from 'jose' +import { + SignJWT, + jwtVerify, + importJWK, + decodeProtectedHeader, + type JWTVerifyResult, + type JWK, +} from 'jose' +import { fetchJwksFromUrl, getKeyFromJwks } from './jwks.js' import type { JwtPayload } from './types.js' /** @@ -58,6 +66,18 @@ export interface EdDSAVerifyConfig extends BaseJwtConfig { publicJwk: JWK } +/** + * EdDSA/RSA asymmetric configuration for verification via HTTP JWKS + * Uses a remote JWKS endpoint to fetch public keys (supports key rotation) + */ +export interface JWKSUrlVerifyConfig extends BaseJwtConfig { + alg: 'EdDSA' | 'RS256' | 'RS384' | 'RS512' + /** HTTP(S) URL to JWKS endpoint */ + jwksUrl: string + /** Cache TTL in seconds (default: 300) */ + cacheTtl?: number +} + /** * Union type for signing configuration */ @@ -66,7 +86,7 @@ export type SignConfig = HS512Config | EdDSASignConfig /** * Union type for verification configuration */ -export type VerifyConfig = HS512Config | EdDSAVerifyConfig +export type VerifyConfig = HS512Config | EdDSAVerifyConfig | JWKSUrlVerifyConfig /** * Sign a JWT token with explicit configuration @@ -116,8 +136,11 @@ export async function signWithConfig( .setExpirationTime(now + ttlSeconds) if (config.alg === 'HS512') { - if (config.secret.length < 32) { - throw new Error(`JWT secret too short: ${config.secret.length} bytes, need >= 32`) + // SECURITY: HS512 requires 64-byte minimum (SHA-512 digest size) + if (config.secret.length < 64) { + throw new Error( + `JWT secret too short: ${config.secret.length} bytes, need >= 64 for HS512` + ) } return jwt.setProtectedHeader({ alg: 'HS512', typ: 'JWT' }).sign(config.secret) } else { @@ -170,9 +193,10 @@ export async function verifyWithConfig( let result: JWTVerifyResult if (config.alg === 'HS512') { - if (config.secret.length < 32) { + // SECURITY: HS512 requires 64-byte minimum (SHA-512 digest size) + if (config.secret.length < 64) { throw new Error( - `JWT secret too short: ${config.secret.length} bytes, need >= 32` + `JWT secret too short: ${config.secret.length} bytes, need >= 64 for HS512` ) } result = await jwtVerify(token, config.secret, { @@ -180,13 +204,28 @@ export async function verifyWithConfig( audience: aud, clockTolerance: leeway, }) - } else { + } else if ('publicJwk' in config) { + // Inline JWK verification const key = await importJWK(config.publicJwk, 'EdDSA') result = await jwtVerify(token, key, { issuer: iss, audience: aud, clockTolerance: leeway, }) + } else if ('jwksUrl' in config) { + // HTTP JWKS verification (NEW) + const header = decodeProtectedHeader(token) + const jwks = await fetchJwksFromUrl(config.jwksUrl, config.cacheTtl) + const key = await getKeyFromJwks(header.kid, jwks) + + result = await jwtVerify(token, key, { + algorithms: ['EdDSA', 'RS256', 'RS384', 'RS512'], + issuer: iss, + audience: aud, + clockTolerance: leeway, + }) + } else { + throw new Error('Invalid verification config') } return result.payload as JwtPayload @@ -392,8 +431,9 @@ export function createHS512Config( const b64 = secret.replace(/-/g, '+').replace(/_/g, '/') const buf = Buffer.from(b64, 'base64') - if (buf.length < 32) { - throw new Error(`JWT secret too short: ${buf.length} bytes, need >= 32`) + // SECURITY: HS512 requires 64-byte minimum (SHA-512 digest size) + if (buf.length < 64) { + throw new Error(`JWT secret too short: ${buf.length} bytes, need >= 64 for HS512`) } return { @@ -445,3 +485,41 @@ export function createEdDSAVerifyConfig( ...baseConfig, } } + +/** + * Helper function to create HTTP JWKS URL verification config + * + * Enables testing without environment variables by providing explicit configuration + * + * @example + * ```typescript + * // Auth0 configuration + * const config = createJWKSUrlVerifyConfig( + * 'https://tenant.auth0.com/.well-known/jwks.json', + * { + * iss: 'https://tenant.auth0.com/', + * aud: 'my-client-id' + * } + * ) + * + * const payload = await verifyWithConfig(token, config) + * ``` + * + * @param jwksUrl - HTTP(S) URL to JWKS endpoint + * @param baseConfig - Base JWT configuration + * @param cacheTtl - Optional cache TTL in seconds (default: 300) + * @returns JWKS URL verification configuration + */ +export function createJWKSUrlVerifyConfig( + jwksUrl: string, + baseConfig: Omit & + Partial>, + cacheTtl?: number +): JWKSUrlVerifyConfig { + return { + alg: 'EdDSA', // Default, will support RSA via JWKS + jwksUrl, + cacheTtl, + ...baseConfig, + } +} diff --git a/packages/flarelette-jwt-ts/src/index.ts b/packages/flarelette-jwt-ts/src/index.ts index 1656509..d64e55c 100644 --- a/packages/flarelette-jwt-ts/src/index.ts +++ b/packages/flarelette-jwt-ts/src/index.ts @@ -17,6 +17,8 @@ export { getPrivateJwkString, getPublicJwkString, getJwksServiceName, + getJwksUrl, + getJwksCacheTtl, } from './config.js' // Signing and verification @@ -61,6 +63,7 @@ export { createHS512Config, createEdDSASignConfig, createEdDSAVerifyConfig, + createJWKSUrlVerifyConfig, } from './explicit.js' export type { @@ -68,6 +71,7 @@ export type { HS512Config, EdDSASignConfig, EdDSAVerifyConfig, + JWKSUrlVerifyConfig, SignConfig, VerifyConfig, AuthzOptsWithConfig, diff --git a/packages/flarelette-jwt-ts/src/jwks.ts b/packages/flarelette-jwt-ts/src/jwks.ts index 86c2e4c..6f86af2 100644 --- a/packages/flarelette-jwt-ts/src/jwks.ts +++ b/packages/flarelette-jwt-ts/src/jwks.ts @@ -4,7 +4,7 @@ * This module provides functions to fetch and manage JWKS, including caching and key lookup by key ID (kid). * It supports integration with external JWKS services. * - * @module util + * @module jwks * */ @@ -20,16 +20,28 @@ interface JWKWithKid extends JsonWebKey { } /** - * JWKS cache with cooldown period + * JWKS cache with cooldown period (service binding) */ interface JWKSCache { keys: JWKWithKid[] fetchedAt: number } +/** + * HTTP JWKS cache entry with configurable TTL + */ +interface HttpJWKSCacheEntry { + keys: JWKWithKid[] + fetchedAt: number + ttl: number // TTL in milliseconds +} + let cache: JWKSCache | null = null const COOLDOWN = 300000 // 5 minutes in milliseconds +// HTTP JWKS cache (separate from service binding cache) +const httpJwksCache = new Map() + /** * Clear the JWKS cache (for testing purposes) * @internal @@ -38,6 +50,14 @@ export function clearJwksCache(): void { cache = null } +/** + * Clear the HTTP JWKS cache (for testing purposes) + * @internal + */ +export function clearHttpJwksCache(): void { + httpJwksCache.clear() +} + /** * Fetch JWKS from a service binding * Implements 5-minute caching to reduce load on JWKS service @@ -69,8 +89,124 @@ export async function fetchJwksFromService(service: Fetcher): Promise { + // Validate URL (fail-fast on config errors) + validateJwksUrl(url) + + // Check cache + const cached = httpJwksCache.get(url) + const now = Date.now() + + if (cached) { + const age = now - cached.fetchedAt + if (age < cached.ttl) { + return cached.keys + } + // Cache expired, remove it + httpJwksCache.delete(url) + } + + // Fetch fresh JWKS + const MAX_JWKS_SIZE_BYTES = 100 * 1024 // 100KB + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + 'User-Agent': 'flarelette-jwt-ts', + }, + signal: AbortSignal.timeout(5000), // 5 second timeout + }) + + if (!response.ok) { + throw new Error( + `JWKS HTTP fetch returned ${response.status}: ${response.statusText}` + ) + } + + // Read as text first to check size + const text = await response.text() + + if (text.length > MAX_JWKS_SIZE_BYTES) { + throw new Error('JWKS response exceeds size limit (100KB)') + } + + // Parse JSON + const data = JSON.parse(text) as JWKSResponse + + if (!data.keys || !Array.isArray(data.keys)) { + throw new Error('Invalid JWKS response: missing keys array') + } + + // Cache the result + const ttlMs = ttlSeconds * 1000 + httpJwksCache.set(url, { + keys: data.keys, + fetchedAt: now, + ttl: ttlMs, + }) + + return data.keys +} + /** * Find and import a specific key from JWKS by kid + * + * Supports both EdDSA (Ed25519) and RSA (RS256/RS384/RS512) keys + * Algorithm is auto-detected from key type (kty) and curve (crv) + * + * @param kid - Key ID from JWT header + * @param jwks - Array of JWK objects + * @returns CryptoKey or Uint8Array suitable for jose verification */ export async function getKeyFromJwks( kid: string | undefined, @@ -82,6 +218,8 @@ export async function getKeyFromJwks( ) } + // SECURITY: Strict equality (===) prevents injection attacks. The kid is treated + // as a pure lookup key, never interpolated into SQL, file paths, or URLs. const jwk = jwks.find(k => k.kid === kid) if (!jwk) { @@ -94,8 +232,10 @@ export async function getKeyFromJwks( ) } - // Cast to JWK type that jose expects - return importJWK(jwk as Parameters[0], 'EdDSA') + // Let jose infer algorithm from JWK structure + // EdDSA: kty=OKP, crv=Ed25519 + // RSA: kty=RSA (algorithm in alg field or inferred) + return importJWK(jwk as Parameters[0]) } /** diff --git a/packages/flarelette-jwt-ts/src/types.ts b/packages/flarelette-jwt-ts/src/types.ts index f9f24bb..9a93f40 100644 --- a/packages/flarelette-jwt-ts/src/types.ts +++ b/packages/flarelette-jwt-ts/src/types.ts @@ -239,6 +239,10 @@ export interface WorkerEnv extends Record { JWT_JWKS_SERVICE?: Fetcher JWT_JWKS_SERVICE_NAME?: string + // HTTP JWKS (TypeScript only - Python support pending) + JWT_JWKS_URL?: string + JWT_JWKS_CACHE_TTL_SECONDS?: string + // Thumbprint pinning JWT_ALLOWED_THUMBPRINTS?: string } diff --git a/packages/flarelette-jwt-ts/src/verify.ts b/packages/flarelette-jwt-ts/src/verify.ts index 39b8854..17f8a3f 100644 --- a/packages/flarelette-jwt-ts/src/verify.ts +++ b/packages/flarelette-jwt-ts/src/verify.ts @@ -4,7 +4,7 @@ * This module provides functions to verify JWT tokens using either HS512 or EdDSA algorithms. * It supports integration with JWKS services and thumbprint pinning. * - * @module util + * @module verify * */ @@ -14,15 +14,119 @@ import { calculateJwkThumbprint, decodeProtectedHeader, } from 'jose' -import { envMode, getCommon, getHSSecret, getPublicJwkString } from './config.js' -import { fetchJwksFromService, getKeyFromJwks, allowedThumbprints } from './jwks.js' +import { + envMode, + getCommon, + getHSSecret, + getPublicJwkString, + getJwksUrl, + getJwksCacheTtl, +} from './config.js' +import { + fetchJwksFromService, + fetchJwksFromUrl, + getKeyFromJwks, + allowedThumbprints, +} from './jwks.js' import type { AlgType, Fetcher, JwtPayload } from './types.js' /** - * Verify a JWT token with HS512 or EdDSA algorithm + * Resolve verification key from configured sources + * + * Implements key resolution strategy pattern: + * - Strategy 1: HS512 shared secret + * - Strategy 2: Inline public JWK + * - Strategy 3: Service binding JWKS + * - Strategy 4: HTTP JWKS URL + * + * @param token - JWT token string + * @param opts - Verification options + * @returns Key and allowed algorithms + * @throws Error if no key source configured + */ +async function resolveVerificationKey( + token: string, + opts?: Partial<{ + jwksService: Fetcher + jwksUrl: string + jwksCacheTtl: number + }> +): Promise<{ key: CryptoKey | Uint8Array; algorithms: string[] }> { + const mode: AlgType = envMode('consumer') + + // Strategy 1: HS512 shared secret + if (mode === 'HS512') { + return { + key: getHSSecret(), + algorithms: ['HS512'], + } + } + + // EdDSA/RSA mode - multiple key sources + const inline = getPublicJwkString() + + // Strategy 2: Inline public JWK + if (inline) { + const jwk = JSON.parse(inline) + // SECURITY: Detect algorithm from JWK structure. If JWK has explicit alg field, jose will use it. + // For EdDSA keys (kty=OKP, crv=Ed25519), explicitly specify 'EdDSA' for compatibility. + // Algorithm whitelist in jwtVerify() provides defense-in-depth protection. + const isEdDSA = jwk.kty === 'OKP' && jwk.crv === 'Ed25519' + const key = isEdDSA ? await importJWK(jwk, 'EdDSA') : await importJWK(jwk) + + // Optional thumbprint pinning + const pins = allowedThumbprints() + if (pins) { + const th = await calculateJwkThumbprint(jwk) + if (!pins.has(th)) { + throw new Error('Public key thumbprint not in allowed list') + } + } + + return { + key, + algorithms: ['EdDSA', 'RS256', 'RS384', 'RS512'], + } + } + + // Strategy 3: Service binding JWKS + if (opts?.jwksService) { + const header = decodeProtectedHeader(token) + const jwks = await fetchJwksFromService(opts.jwksService) + const key = await getKeyFromJwks(header.kid, jwks) + + return { + key, + algorithms: ['EdDSA', 'RS256', 'RS384', 'RS512'], + } + } + + // Strategy 4: HTTP JWKS URL (NEW) + const jwksUrl = opts?.jwksUrl ?? getJwksUrl() + if (jwksUrl) { + const header = decodeProtectedHeader(token) + const cacheTtl = opts?.jwksCacheTtl ?? getJwksCacheTtl() + const jwks = await fetchJwksFromUrl(jwksUrl, cacheTtl) + const key = await getKeyFromJwks(header.kid, jwks) + + return { + key, + algorithms: ['EdDSA', 'RS256', 'RS384', 'RS512'], + } + } + + throw new Error( + 'Verification requires JWT_SECRET, JWT_PUBLIC_JWK, JWT_JWKS_SERVICE, or JWT_JWKS_URL' + ) +} + +/** + * Verify a JWT token with HS512, EdDSA, or RSA algorithms + * + * Supports multiple key resolution strategies with automatic algorithm detection * * @param token - JWT token string to verify - * @param opts - Optional overrides for iss, aud, leeway, and jwksService + * @param opts - Optional overrides for iss, aud, leeway, jwksService, jwksUrl, jwksCacheTtl * @returns Decoded payload if valid, null otherwise */ export async function verify( @@ -32,71 +136,25 @@ export async function verify( aud: string | string[] leeway: number jwksService: Fetcher + jwksUrl: string + jwksCacheTtl: number }> ): Promise { - const mode: AlgType = envMode('consumer') const { iss, aud, leeway } = { ...getCommon(), ...(opts || {}) } - if (mode === 'HS512') { - try { - const key = getHSSecret() - const { payload } = await jwtVerify(token, key, { - algorithms: ['HS512'], - issuer: iss, - audience: aud, - clockTolerance: leeway, - }) - return payload - } catch { - return null - } - } else { - // EdDSA mode - try { - let keyLike: CryptoKey | Uint8Array - const inline = getPublicJwkString() - - if (inline) { - // Strategy 1: Inline public JWK (single key) - const jwk = JSON.parse(inline) - keyLike = await importJWK(jwk, 'EdDSA') - - // Optional: Verify thumbprint if pinning configured - const pins = allowedThumbprints() - if (pins) { - const th = await calculateJwkThumbprint(jwk) - if (!pins.has(th)) { - throw new Error('Public key thumbprint not in allowed list') - } - } - } else if (opts?.jwksService) { - // Strategy 2: Service binding JWKS (key set with rotation) - const header = decodeProtectedHeader(token) - const jwks = await fetchJwksFromService(opts.jwksService) - keyLike = await getKeyFromJwks(header.kid, jwks) - } else { - throw new Error( - 'EdDSA verification requires JWT_PUBLIC_JWK or JWT_JWKS_SERVICE' - ) - } + try { + // All strategies use the same verification logic + const { key, algorithms } = await resolveVerificationKey(token, opts) - const res = await jwtVerify(token, keyLike, { - algorithms: ['EdDSA'], - issuer: iss, - audience: aud, - clockTolerance: leeway, - }) - - // For JWKS verification, verify thumbprint if pinning configured - const pins = allowedThumbprints() - if (pins && opts?.jwksService) { - // Note: jose doesn't expose the key in the result, so we skip thumbprint verification for JWKS - // In production, configure JWT_ALLOWED_THUMBPRINTS to pin specific keys - } + const { payload } = await jwtVerify(token, key, { + algorithms, + issuer: iss, + audience: aud, + clockTolerance: leeway, + }) - return res.payload - } catch { - return null - } + return payload + } catch { + return null // Fail-silent pattern } } diff --git a/packages/flarelette-jwt-ts/tests/config.test.ts b/packages/flarelette-jwt-ts/tests/config.test.ts index 86af712..f4041dc 100644 --- a/packages/flarelette-jwt-ts/tests/config.test.ts +++ b/packages/flarelette-jwt-ts/tests/config.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { envMode, getCommon, getHSSecret } from '../src/config' +import { + envMode, + getCommon, + getHSSecret, + getJwksUrl, + getJwksCacheTtl, +} from '../src/config' describe('Config - envMode', () => { beforeEach(() => { @@ -155,7 +161,7 @@ describe('Config - getHSSecret', () => { const key = getHSSecret() expect(key).toBeInstanceOf(Uint8Array) - expect(key.length).toBeGreaterThanOrEqual(32) + expect(key.length).toBeGreaterThanOrEqual(64) }) it('should use secret-name indirection', () => { @@ -165,7 +171,7 @@ describe('Config - getHSSecret', () => { const key = getHSSecret() expect(key).toBeInstanceOf(Uint8Array) - expect(key.length).toBeGreaterThanOrEqual(32) + expect(key.length).toBeGreaterThanOrEqual(64) }) it('should prefer JWT_SECRET_NAME over JWT_SECRET', () => { @@ -209,24 +215,194 @@ describe('Config - getHSSecret', () => { expect(key.length).toBe(64) }) - it('should fallback to UTF-8 for non-base64 secret', () => { - // A 64-character string that's not valid base64 - const secret = 'x'.repeat(64) + it('should handle base64 with standard encoding', () => { + const bytes = new Uint8Array(64) + for (let i = 0; i < 64; i++) bytes[i] = i + const secret = Buffer.from(bytes).toString('base64') process.env.JWT_SECRET = secret const key = getHSSecret() - expect(key).toBeInstanceOf(Uint8Array) - // UTF-8 encoding produces 64 bytes for 64 ASCII characters - expect(key.length).toBeGreaterThanOrEqual(32) + expect(key.length).toBe(64) }) - it('should handle base64 with standard encoding', () => { + it('should enforce 64-byte minimum for HS512', () => { + // 63 bytes should fail + const bytes = new Uint8Array(63) + for (let i = 0; i < 63; i++) bytes[i] = i + const secret = Buffer.from(bytes).toString('base64url') + process.env.JWT_SECRET = secret + + expect(() => getHSSecret()).toThrow( + 'JWT secret too short: 63 bytes, need >= 64 for HS512' + ) + }) + + it('should accept exactly 64 bytes', () => { const bytes = new Uint8Array(64) for (let i = 0; i < 64; i++) bytes[i] = i - const secret = Buffer.from(bytes).toString('base64') + const secret = Buffer.from(bytes).toString('base64url') process.env.JWT_SECRET = secret const key = getHSSecret() expect(key.length).toBe(64) }) }) + +describe('Config - JWT_JWKS_URL Detection', () => { + beforeEach(() => { + delete process.env.JWT_PUBLIC_JWK + delete process.env.JWT_PUBLIC_JWK_NAME + delete process.env.JWT_JWKS_SERVICE + delete process.env.JWT_JWKS_SERVICE_NAME + delete process.env.JWT_JWKS_URL + delete process.env.JWT_SECRET + delete process.env.JWT_SECRET_NAME + }) + + afterEach(() => { + delete (globalThis as { __FLARELETTE_ENV?: unknown }).__FLARELETTE_ENV + }) + + it('should detect EdDSA mode for consumer with JWT_JWKS_URL', () => { + process.env.JWT_JWKS_URL = 'https://tenant.auth0.com/.well-known/jwks.json' + expect(envMode('consumer')).toBe('EdDSA') + }) + + it('should not detect JWT_JWKS_URL for producer', () => { + process.env.JWT_JWKS_URL = 'https://example.com/.well-known/jwks.json' + // Producer should default to HS512 since no private key + expect(envMode('producer')).toBe('HS512') + }) + + it('should throw error if both HS512 and asymmetric secrets configured', () => { + process.env.JWT_SECRET = Buffer.from('a'.repeat(64)).toString('base64url') + process.env.JWT_JWKS_URL = 'https://example.com/.well-known/jwks.json' + + expect(() => envMode('consumer')).toThrow( + 'Configuration error: Both HS512 (JWT_SECRET) and asymmetric (JWT_PUBLIC_JWK/JWT_JWKS_*) secrets configured' + ) + }) + + it('should throw error if JWT_SECRET_NAME and JWT_PUBLIC_JWK both configured', () => { + process.env.JWT_SECRET_NAME = 'MY_SECRET' + process.env.JWT_PUBLIC_JWK = '{"kty":"OKP"}' + + expect(() => envMode('consumer')).toThrow( + 'Configuration error: Both HS512 (JWT_SECRET) and asymmetric' + ) + }) + + it('should throw error if JWT_SECRET and JWT_JWKS_SERVICE configured', () => { + process.env.JWT_SECRET = Buffer.from('a'.repeat(64)).toString('base64url') + process.env.JWT_JWKS_SERVICE = 'my-jwks-service' + + expect(() => envMode('consumer')).toThrow('Configuration error') + }) + + it('should allow JWT_JWKS_URL without JWT_SECRET', () => { + process.env.JWT_JWKS_URL = 'https://example.com/.well-known/jwks.json' + expect(() => envMode('consumer')).not.toThrow() + expect(envMode('consumer')).toBe('EdDSA') + }) + + it('should allow JWT_SECRET without JWT_JWKS_URL', () => { + process.env.JWT_SECRET = Buffer.from('a'.repeat(64)).toString('base64url') + expect(() => envMode('consumer')).not.toThrow() + expect(envMode('consumer')).toBe('HS512') + }) +}) + +describe('Config - getJwksUrl', () => { + beforeEach(() => { + delete process.env.JWT_JWKS_URL + }) + + afterEach(() => { + delete (globalThis as { __FLARELETTE_ENV?: unknown }).__FLARELETTE_ENV + }) + + it('should return null when JWT_JWKS_URL not set', () => { + expect(getJwksUrl()).toBeNull() + }) + + it('should read JWT_JWKS_URL from process.env', () => { + process.env.JWT_JWKS_URL = 'https://tenant.auth0.com/.well-known/jwks.json' + expect(getJwksUrl()).toBe('https://tenant.auth0.com/.well-known/jwks.json') + }) + + it('should read JWT_JWKS_URL from __FLARELETTE_ENV', () => { + ;(globalThis as { __FLARELETTE_ENV?: Record }).__FLARELETTE_ENV = { + JWT_JWKS_URL: 'https://example.com/.well-known/jwks.json', + } + expect(getJwksUrl()).toBe('https://example.com/.well-known/jwks.json') + }) + + it('should prefer __FLARELETTE_ENV over process.env', () => { + process.env.JWT_JWKS_URL = 'https://process.example.com/jwks.json' + ;(globalThis as { __FLARELETTE_ENV?: Record }).__FLARELETTE_ENV = { + JWT_JWKS_URL: 'https://global.example.com/jwks.json', + } + expect(getJwksUrl()).toBe('https://global.example.com/jwks.json') + }) +}) + +describe('Config - getJwksCacheTtl', () => { + beforeEach(() => { + delete process.env.JWT_JWKS_CACHE_TTL_SECONDS + }) + + afterEach(() => { + delete (globalThis as { __FLARELETTE_ENV?: unknown }).__FLARELETTE_ENV + }) + + it('should return default 300 seconds when not set', () => { + expect(getJwksCacheTtl()).toBe(300) + }) + + it('should read custom TTL from process.env', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = '600' + expect(getJwksCacheTtl()).toBe(600) + }) + + it('should read TTL from __FLARELETTE_ENV', () => { + ;(globalThis as { __FLARELETTE_ENV?: Record }).__FLARELETTE_ENV = { + JWT_JWKS_CACHE_TTL_SECONDS: '900', + } + expect(getJwksCacheTtl()).toBe(900) + }) + + it('should accept 0 as valid TTL (no caching)', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = '0' + expect(getJwksCacheTtl()).toBe(0) + }) + + it('should throw error for negative TTL', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = '-60' + expect(() => getJwksCacheTtl()).toThrow( + 'JWT_JWKS_CACHE_TTL_SECONDS must be a positive number' + ) + }) + + it('should throw error for non-numeric TTL', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = 'not-a-number' + expect(() => getJwksCacheTtl()).toThrow( + 'JWT_JWKS_CACHE_TTL_SECONDS must be a positive number' + ) + }) + + it('should throw error for empty string', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = '' + // Empty string should use default, not throw + expect(getJwksCacheTtl()).toBe(300) + }) + + it('should accept large TTL values', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = '86400' // 24 hours + expect(getJwksCacheTtl()).toBe(86400) + }) + + it('should handle decimal values by converting to integer', () => { + process.env.JWT_JWKS_CACHE_TTL_SECONDS = '300.5' + expect(getJwksCacheTtl()).toBe(300.5) + }) +}) diff --git a/packages/flarelette-jwt-ts/tests/explicit-jwks.test.ts b/packages/flarelette-jwt-ts/tests/explicit-jwks.test.ts new file mode 100644 index 0000000..cfecee9 --- /dev/null +++ b/packages/flarelette-jwt-ts/tests/explicit-jwks.test.ts @@ -0,0 +1,816 @@ +import { describe, test, expect, beforeEach, vi } from 'vitest' +import { + createJWKSUrlVerifyConfig, + verifyWithConfig, + createHS512Config, + createEdDSASignConfig, + checkAuthWithConfig, +} from '../src/explicit' +import { clearHttpJwksCache } from '../src/jwks' +import { SignJWT, generateKeyPair, exportJWK } from 'jose' + +describe('explicit-jwks.test.ts - Explicit Configuration API', () => { + beforeEach(() => { + // Clear HTTP JWKS cache before each test + clearHttpJwksCache() + + // Ensure environment variables don't interfere + delete process.env.JWT_SECRET + delete process.env.JWT_ISS + delete process.env.JWT_AUD + delete process.env.JWT_PUBLIC_JWK + delete process.env.JWT_JWKS_URL + }) + + describe('createJWKSUrlVerifyConfig', () => { + test('should create config without environment variables', () => { + const config = createJWKSUrlVerifyConfig( + 'https://tenant.auth0.com/.well-known/jwks.json', + { + iss: 'https://tenant.auth0.com/', + aud: 'my-app-client-id', + } + ) + + expect(config).toEqual({ + alg: 'EdDSA', + jwksUrl: 'https://tenant.auth0.com/.well-known/jwks.json', + cacheTtl: undefined, + iss: 'https://tenant.auth0.com/', + aud: 'my-app-client-id', + }) + }) + + test('should accept custom cache TTL', () => { + const config = createJWKSUrlVerifyConfig( + 'https://tenant.auth0.com/.well-known/jwks.json', + { + iss: 'https://tenant.auth0.com/', + aud: 'my-app', + }, + 600 // 10 minutes + ) + + expect(config.cacheTtl).toBe(600) + }) + + test('should accept optional ttlSeconds', () => { + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + ttlSeconds: 1800, + } + ) + + expect(config.ttlSeconds).toBe(1800) + }) + + test('should accept optional leeway', () => { + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + leeway: 120, + } + ) + + expect(config.leeway).toBe(120) + }) + }) + + describe('verifyWithConfig - JWKS URL', () => { + test('should verify token using HTTP JWKS without environment', async () => { + // Generate EdDSA keypair for testing + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'test-key-1' + publicJWK.alg = 'EdDSA' // Required by jose importJWK + publicJWK.use = 'sig' + + // Create and sign a token + const token = await new SignJWT({ sub: 'user123', permissions: ['read:data'] }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'test-key-1' }) + .setIssuer('https://test-issuer.com/') + .setAudience('test-audience') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + // Mock fetch to return JWKS + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + // Create config without environment variables + const config = createJWKSUrlVerifyConfig( + 'https://test-issuer.com/.well-known/jwks.json', + { + iss: 'https://test-issuer.com/', + aud: 'test-audience', + } + ) + + // Verify token + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeDefined() + expect(payload?.sub).toBe('user123') + expect(payload?.permissions).toEqual(['read:data']) + }) + + test('should return null for invalid token', async () => { + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + // Invalid token + const payload = await verifyWithConfig('invalid.jwt.token', config) + + expect(payload).toBeNull() + }) + + test('should return null for expired token', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'expired-key' + publicJWK.alg = 'EdDSA' + + // Create expired token + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'expired-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('-1h') // Expired 1 hour ago + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeNull() + }) + + test('should return null for wrong issuer', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'test-key' + publicJWK.alg = 'EdDSA' + + // Token with different issuer + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'test-key' }) + .setIssuer('https://wrong-issuer.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://correct-issuer.com/', // Different issuer + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeNull() + }) + + test('should return null for wrong audience', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'test-key' + publicJWK.alg = 'EdDSA' + + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'test-key' }) + .setIssuer('https://example.com/') + .setAudience('wrong-audience') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'correct-audience', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeNull() + }) + + test('should return null when key not found in JWKS', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'available-key' + publicJWK.alg = 'EdDSA' + + // Token uses different kid + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'missing-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeNull() + }) + + test('should use custom leeway from config', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'test-key' + publicJWK.alg = 'EdDSA' + + // Token that's slightly expired (within custom leeway) + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'test-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1s') // Will be expired soon + .sign(privateKey) + + // Wait for token to expire + await new Promise(resolve => setTimeout(resolve, 1100)) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + leeway: 300, // 5 minutes leeway + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeDefined() + expect(payload?.sub).toBe('user123') + }) + }) + + describe('RSA Token Verification', () => { + test('should verify RSA RS256 token using HTTP JWKS', async () => { + // Generate RSA keypair + const { privateKey, publicKey } = await generateKeyPair('RS256') + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'rsa-key-1' + publicJWK.alg = 'RS256' + publicJWK.use = 'sig' + + // Create RSA-signed token + const token = await new SignJWT({ sub: 'user123', role: 'admin' }) + .setProtectedHeader({ alg: 'RS256', kid: 'rsa-key-1' }) + .setIssuer('https://auth0.example.com/') + .setAudience('api-client-id') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://auth0.example.com/.well-known/jwks.json', + { + iss: 'https://auth0.example.com/', + aud: 'api-client-id', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload).toBeDefined() + expect(payload?.sub).toBe('user123') + expect(payload?.role).toBe('admin') + }) + + test('should verify RSA RS384 token', async () => { + const { privateKey, publicKey } = await generateKeyPair('RS384') + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'rsa-384-key' + publicJWK.alg = 'RS384' + + const token = await new SignJWT({ sub: 'user456' }) + .setProtectedHeader({ alg: 'RS384', kid: 'rsa-384-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('user456') + }) + + test('should verify RSA RS512 token', async () => { + const { privateKey, publicKey } = await generateKeyPair('RS512') + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'rsa-512-key' + publicJWK.alg = 'RS512' + + const token = await new SignJWT({ sub: 'user789' }) + .setProtectedHeader({ alg: 'RS512', kid: 'rsa-512-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('user789') + }) + + test('should handle JWKS with multiple RSA keys', async () => { + const { privateKey: _pk1, publicKey: pub1 } = await generateKeyPair('RS256') + const { privateKey: pk2, publicKey: pub2 } = await generateKeyPair('RS256') + + const jwk1 = await exportJWK(pub1) + jwk1.kid = 'old-key' + jwk1.alg = 'RS256' + + const jwk2 = await exportJWK(pub2) + jwk2.kid = 'new-key' + jwk2.alg = 'RS256' + + // Sign with new key + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'RS256', kid: 'new-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(pk2) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [jwk1, jwk2] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('user123') + }) + + test('should handle mixed EdDSA and RSA keys in JWKS', async () => { + const { privateKey: rsaKey, publicKey: rsaPub } = await generateKeyPair('RS256') + const { privateKey: _edKey, publicKey: edPub } = await generateKeyPair('EdDSA', { + extractable: true, + }) + + const rsaJWK = await exportJWK(rsaPub) + rsaJWK.kid = 'rsa-key' + rsaJWK.alg = 'RS256' + + const edJWK = await exportJWK(edPub) + edJWK.kid = 'ed-key' + edJWK.alg = 'EdDSA' + + // Sign with RSA + const token = await new SignJWT({ sub: 'rsa-user' }) + .setProtectedHeader({ alg: 'RS256', kid: 'rsa-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(rsaKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [edJWK, rsaJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('rsa-user') + }) + }) + + describe('checkAuthWithConfig - JWKS URL', () => { + test('should authorize user with valid token', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'auth-key' + publicJWK.alg = 'EdDSA' + + const token = await new SignJWT({ + sub: 'user123', + permissions: ['read:data', 'write:data'], + roles: ['admin'], + }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'auth-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const authUser = await checkAuthWithConfig(token, config, { + require_all_permissions: ['read:data'], + require_roles_any: ['admin'], + }) + + expect(authUser).toBeDefined() + expect(authUser?.sub).toBe('user123') + expect(authUser?.permissions).toContain('read:data') + expect(authUser?.roles).toContain('admin') + }) + + test('should reject user without required permissions', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'auth-key' + publicJWK.alg = 'EdDSA' + + const token = await new SignJWT({ + sub: 'user123', + permissions: ['read:data'], + }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'auth-key' }) + .setIssuer('https://example.com/') + .setAudience('my-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://example.com/.well-known/jwks.json', + { + iss: 'https://example.com/', + aud: 'my-app', + } + ) + + const authUser = await checkAuthWithConfig(token, config, { + require_all_permissions: ['write:data'], // User doesn't have this + }) + + expect(authUser).toBeNull() + }) + + test('should work without environment variables', async () => { + // Explicitly verify no env vars are set + expect(process.env.JWT_ISS).toBeUndefined() + expect(process.env.JWT_AUD).toBeUndefined() + expect(process.env.JWT_JWKS_URL).toBeUndefined() + + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'test-key' + publicJWK.alg = 'EdDSA' + + const token = await new SignJWT({ sub: 'user123' }) + .setProtectedHeader({ alg: 'EdDSA', kid: 'test-key' }) + .setIssuer('https://explicit-config.com/') + .setAudience('explicit-app') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://explicit-config.com/.well-known/jwks.json', + { + iss: 'https://explicit-config.com/', + aud: 'explicit-app', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('user123') + }) + }) + + describe('Cross-Mode Testing - No Environment Pollution', () => { + test('should work alongside HS512 config without interference', async () => { + // Create HS512 secret (base64url string) + const secretBytes = new Uint8Array(64) + for (let i = 0; i < 64; i++) secretBytes[i] = i + const secret = Buffer.from(secretBytes).toString('base64url') + + const hs512Config = createHS512Config(secret, { + iss: 'https://hs512-issuer.com/', + aud: 'hs512-app', + }) + + // Create JWKS config + const jwksConfig = createJWKSUrlVerifyConfig( + 'https://jwks-issuer.com/.well-known/jwks.json', + { + iss: 'https://jwks-issuer.com/', + aud: 'jwks-app', + } + ) + + // Both configs should coexist without env vars + expect(hs512Config.alg).toBe('HS512') + expect(jwksConfig.alg).toBe('EdDSA') + expect(process.env.JWT_ISS).toBeUndefined() + }) + + test('should work alongside EdDSA inline config', async () => { + const { privateKey, publicKey } = await generateKeyPair('EdDSA', { + extractable: true, + }) + const privateJWK = await exportJWK(privateKey) + const _publicJWK = await exportJWK(publicKey) + + const signConfig = createEdDSASignConfig(privateJWK, 'test-kid', { + iss: 'https://inline-issuer.com/', + aud: 'inline-app', + }) + + const jwksConfig = createJWKSUrlVerifyConfig( + 'https://jwks-issuer.com/.well-known/jwks.json', + { + iss: 'https://jwks-issuer.com/', + aud: 'jwks-app', + } + ) + + // Both configs should work independently + expect(signConfig.alg).toBe('EdDSA') + expect(jwksConfig.alg).toBe('EdDSA') + expect(jwksConfig.jwksUrl).toBe('https://jwks-issuer.com/.well-known/jwks.json') + }) + }) + + describe('Integration-Style Tests', () => { + test('should simulate Auth0 verification flow', async () => { + const { privateKey, publicKey } = await generateKeyPair('RS256') + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'auth0-key-123' + publicJWK.alg = 'RS256' + publicJWK.use = 'sig' + + // Simulate Auth0 token + const token = await new SignJWT({ + sub: 'auth0|123456', + email: 'user@example.com', + permissions: ['read:profile'], + }) + .setProtectedHeader({ alg: 'RS256', kid: 'auth0-key-123' }) + .setIssuer('https://tenant.auth0.com/') + .setAudience('my-api-client-id') + .setIssuedAt() + .setExpirationTime('24h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://tenant.auth0.com/.well-known/jwks.json', + { + iss: 'https://tenant.auth0.com/', + aud: 'my-api-client-id', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('auth0|123456') + expect(payload?.email).toBe('user@example.com') + }) + + test('should simulate Okta verification flow', async () => { + const { privateKey, publicKey } = await generateKeyPair('RS256') + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'okta-key-abc' + publicJWK.alg = 'RS256' + + const token = await new SignJWT({ + sub: '00u123456', + email: 'user@company.com', + groups: ['Everyone', 'Developers'], + }) + .setProtectedHeader({ alg: 'RS256', kid: 'okta-key-abc' }) + .setIssuer('https://company.okta.com/oauth2/default') + .setAudience('api://default') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://company.okta.com/oauth2/default/v1/keys', + { + iss: 'https://company.okta.com/oauth2/default', + aud: 'api://default', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('00u123456') + expect(payload?.groups).toContain('Developers') + }) + + test('should simulate Google OAuth verification flow', async () => { + const { privateKey, publicKey } = await generateKeyPair('RS256') + const publicJWK = await exportJWK(publicKey) + publicJWK.kid = 'google-key-xyz' + publicJWK.alg = 'RS256' + + const token = await new SignJWT({ + sub: '1234567890', + email: 'user@gmail.com', + email_verified: true, + name: 'Test User', + }) + .setProtectedHeader({ alg: 'RS256', kid: 'google-key-xyz' }) + .setIssuer('https://accounts.google.com') + .setAudience('123456-abcdefg.apps.googleusercontent.com') + .setIssuedAt() + .setExpirationTime('1h') + .sign(privateKey) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [publicJWK] }), + }) + + const config = createJWKSUrlVerifyConfig( + 'https://www.googleapis.com/oauth2/v3/certs', + { + iss: 'https://accounts.google.com', + aud: '123456-abcdefg.apps.googleusercontent.com', + } + ) + + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('1234567890') + expect(payload?.email).toBe('user@gmail.com') + expect(payload?.email_verified).toBe(true) + }) + }) +}) diff --git a/packages/flarelette-jwt-ts/tests/explicit.test.ts b/packages/flarelette-jwt-ts/tests/explicit.test.ts index 2c71a2f..4370931 100644 --- a/packages/flarelette-jwt-ts/tests/explicit.test.ts +++ b/packages/flarelette-jwt-ts/tests/explicit.test.ts @@ -21,7 +21,7 @@ describe('Explicit Configuration API', () => { beforeEach(() => { // Create a fresh config for each test - no environment pollution! - const secret = new Uint8Array(32).fill(42) // 32 bytes, all set to 42 + const secret = new Uint8Array(64).fill(42) // 64 bytes (HS512 minimum) testConfig = { alg: 'HS512', secret, @@ -73,10 +73,10 @@ describe('Explicit Configuration API', () => { expect(payload?.sub).toBe('user123') }) - it('should reject secrets shorter than 32 bytes', async () => { + it('should reject secrets shorter than 64 bytes', async () => { const shortConfig = { ...testConfig, - secret: new Uint8Array(16), // Too short! + secret: new Uint8Array(63), // Too short (need 64 for HS512)! } await expect(signWithConfig({ sub: 'test' }, shortConfig)).rejects.toThrow( @@ -375,8 +375,8 @@ describe('Explicit Configuration API', () => { describe('createHS512Config helper', () => { it('should create config from base64url secret', () => { - // Create a base64url-encoded secret (32 bytes) - const secret = Buffer.alloc(32, 42).toString('base64url') + // Create a base64url-encoded secret (64 bytes minimum for HS512) + const secret = Buffer.alloc(64, 42).toString('base64url') const config = createHS512Config(secret, { iss: 'test-issuer', @@ -385,7 +385,7 @@ describe('Explicit Configuration API', () => { expect(config.alg).toBe('HS512') expect(config.secret).toBeInstanceOf(Uint8Array) - expect(config.secret.length).toBe(32) + expect(config.secret.length).toBe(64) expect(config.iss).toBe('test-issuer') expect(config.aud).toBe('test-audience') }) @@ -411,7 +411,7 @@ describe('Explicit Configuration API', () => { try { const config = { alg: 'HS512' as const, - secret: new Uint8Array(32).fill(1), + secret: new Uint8Array(64).fill(1), iss: 'isolated-test', aud: 'isolated-audience', } @@ -429,14 +429,14 @@ describe('Explicit Configuration API', () => { it('should allow multiple configs in same process', async () => { const config1 = { alg: 'HS512' as const, - secret: new Uint8Array(32).fill(1), + secret: new Uint8Array(64).fill(1), iss: 'issuer-1', aud: 'audience-1', } const config2 = { alg: 'HS512' as const, - secret: new Uint8Array(32).fill(2), + secret: new Uint8Array(64).fill(2), iss: 'issuer-2', aud: 'audience-2', } diff --git a/packages/flarelette-jwt-ts/tests/jwks-http.test.ts b/packages/flarelette-jwt-ts/tests/jwks-http.test.ts new file mode 100644 index 0000000..d9e2536 --- /dev/null +++ b/packages/flarelette-jwt-ts/tests/jwks-http.test.ts @@ -0,0 +1,666 @@ +import { describe, test, expect, beforeEach, vi } from 'vitest' +import { fetchJwksFromUrl, clearHttpJwksCache } from '../src/jwks' + +// Mock fetch globally +const originalFetch = globalThis.fetch + +describe('jwks-http.ts - HTTP JWKS Fetching', () => { + beforeEach(() => { + // Clear HTTP JWKS cache before each test + clearHttpJwksCache() + + // Restore original fetch + globalThis.fetch = originalFetch + }) + + describe('URL Validation', () => { + test('should reject invalid URL format', async () => { + await expect(fetchJwksFromUrl('not-a-url')).rejects.toThrow( + 'JWT_JWKS_URL must be a valid URL' + ) + }) + + test('should reject empty URL', async () => { + await expect(fetchJwksFromUrl('')).rejects.toThrow( + 'JWT_JWKS_URL must be a valid URL' + ) + }) + + test('should reject http:// URLs (non-localhost)', async () => { + await expect( + fetchJwksFromUrl('http://example.com/.well-known/jwks.json') + ).rejects.toThrow('JWT_JWKS_URL must use HTTPS (except localhost for testing)') + }) + + test('should allow http://localhost for testing', async () => { + const mockKeys = [{ kid: 'test-key', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + }) + + const result = await fetchJwksFromUrl( + 'http://localhost:3000/.well-known/jwks.json' + ) + expect(result).toEqual(mockKeys) + }) + + test('should allow http://127.0.0.1 for testing', async () => { + const mockKeys = [{ kid: 'test-key', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + }) + + const result = await fetchJwksFromUrl( + 'http://127.0.0.1:3000/.well-known/jwks.json' + ) + expect(result).toEqual(mockKeys) + }) + + test('should allow http://[::1] (IPv6 localhost) for testing', async () => { + const mockKeys = [{ kid: 'test-key', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + }) + + const result = await fetchJwksFromUrl('http://[::1]:3000/.well-known/jwks.json') + expect(result).toEqual(mockKeys) + }) + + test('should allow https:// URLs', async () => { + const mockKeys = [{ kid: 'test-key', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + }) + + const result = await fetchJwksFromUrl( + 'https://tenant.auth0.com/.well-known/jwks.json' + ) + expect(result).toEqual(mockKeys) + }) + + test('should reject ftp:// protocol', async () => { + await expect( + fetchJwksFromUrl('ftp://example.com/.well-known/jwks.json') + ).rejects.toThrow('JWT_JWKS_URL must use HTTPS') + }) + + test('should reject file:// protocol', async () => { + await expect(fetchJwksFromUrl('file:///etc/jwks.json')).rejects.toThrow( + 'JWT_JWKS_URL must use HTTPS' + ) + }) + }) + + describe('HTTP Fetching', () => { + test('should fetch JWKS successfully', async () => { + const mockKeys = [ + { kid: 'key1', kty: 'RSA', use: 'sig', n: 'abc', e: 'AQAB' }, + { kid: 'key2', kty: 'OKP', crv: 'Ed25519', x: 'xyz' }, + ] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + }) + + const result = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(result).toEqual(mockKeys) + expect(globalThis.fetch).toHaveBeenCalledWith( + 'https://example.com/.well-known/jwks.json', + expect.objectContaining({ + method: 'GET', + headers: { + Accept: 'application/json', + 'User-Agent': 'flarelette-jwt-ts', + }, + }) + ) + }) + + test('should throw error on 404 Not Found', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('JWKS HTTP fetch returned 404: Not Found') + }) + + test('should throw error on 500 Internal Server Error', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('JWKS HTTP fetch returned 500: Internal Server Error') + }) + + test('should throw error on 403 Forbidden', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 403, + statusText: 'Forbidden', + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('JWKS HTTP fetch returned 403: Forbidden') + }) + + test('should throw error on network timeout', async () => { + // Simulate timeout by rejecting after delay + globalThis.fetch = vi.fn().mockRejectedValue(new Error('AbortError')) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow() + }) + + test('should throw error on malformed JSON', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => 'not valid json{{{', + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow() + }) + + test('should throw error if response missing keys array', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ data: 'invalid' }), + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('Invalid JWKS response: missing keys array') + }) + + test('should throw error if keys is not an array', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: 'not-an-array' }), + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('Invalid JWKS response: missing keys array') + }) + + test('should handle empty keys array', async () => { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [] }), + }) + + const result = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(result).toEqual([]) + }) + + test('should include timeout in fetch options', async () => { + const mockKeys = [{ kid: 'key1', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + }) + + await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(globalThis.fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + signal: expect.any(AbortSignal), + }) + ) + }) + }) + + describe('Caching', () => { + test('should cache JWKS for default TTL (5 minutes)', async () => { + let fetchCount = 0 + const mockKeys = [{ kid: 'key1', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockImplementation(async () => { + fetchCount++ + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + } + }) + + const url = 'https://example.com/.well-known/jwks.json' + + // First fetch + const result1 = await fetchJwksFromUrl(url) + expect(result1).toEqual(mockKeys) + expect(fetchCount).toBe(1) + + // Second fetch (should use cache) + const result2 = await fetchJwksFromUrl(url) + expect(result2).toEqual(mockKeys) + expect(fetchCount).toBe(1) // Still 1, not fetched again + }) + + test('should respect custom cache TTL', async () => { + let fetchCount = 0 + const mockKeys = [{ kid: 'key1', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockImplementation(async () => { + fetchCount++ + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + } + }) + + const url = 'https://example.com/.well-known/jwks.json' + const ttl = 600 // 10 minutes + + // First fetch with custom TTL + const result1 = await fetchJwksFromUrl(url, ttl) + expect(result1).toEqual(mockKeys) + expect(fetchCount).toBe(1) + + // Second fetch (should use cache) + const result2 = await fetchJwksFromUrl(url, ttl) + expect(result2).toEqual(mockKeys) + expect(fetchCount).toBe(1) + }) + + test('should cache per URL', async () => { + const mockKeys1 = [{ kid: 'key1', kty: 'OKP' }] + const mockKeys2 = [{ kid: 'key2', kty: 'RSA' }] + + globalThis.fetch = vi.fn().mockImplementation(async (url: string) => { + const keys = url.includes('auth0') ? mockKeys1 : mockKeys2 + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ keys }), + } + }) + + const url1 = 'https://tenant.auth0.com/.well-known/jwks.json' + const url2 = 'https://domain.okta.com/.well-known/jwks.json' + + // Fetch from two different URLs + const result1 = await fetchJwksFromUrl(url1) + const result2 = await fetchJwksFromUrl(url2) + + expect(result1).toEqual(mockKeys1) + expect(result2).toEqual(mockKeys2) + expect(globalThis.fetch).toHaveBeenCalledTimes(2) + }) + + test('should expire cache after TTL', async () => { + let fetchCount = 0 + const mockKeys = [{ kid: 'key1', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockImplementation(async () => { + fetchCount++ + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + } + }) + + const url = 'https://example.com/.well-known/jwks.json' + const ttl = 0.1 // 100ms for testing + + // First fetch + await fetchJwksFromUrl(url, ttl) + expect(fetchCount).toBe(1) + + // Wait for cache to expire + await new Promise(resolve => setTimeout(resolve, 150)) + + // Second fetch (should fetch again) + await fetchJwksFromUrl(url, ttl) + expect(fetchCount).toBe(2) + }) + + test('should clear cache on demand', async () => { + let fetchCount = 0 + const mockKeys = [{ kid: 'key1', kty: 'OKP' }] + + globalThis.fetch = vi.fn().mockImplementation(async () => { + fetchCount++ + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: mockKeys }), + } + }) + + const url = 'https://example.com/.well-known/jwks.json' + + // First fetch + await fetchJwksFromUrl(url) + expect(fetchCount).toBe(1) + + // Clear cache + clearHttpJwksCache() + + // Second fetch (should fetch again due to cleared cache) + await fetchJwksFromUrl(url) + expect(fetchCount).toBe(2) + }) + + test('should update cache on re-fetch after expiry', async () => { + const mockKeys1 = [{ kid: 'old-key', kty: 'OKP' }] + const mockKeys2 = [{ kid: 'new-key', kty: 'OKP' }] + + let callCount = 0 + globalThis.fetch = vi.fn().mockImplementation(async () => { + callCount++ + const keys = callCount === 1 ? mockKeys1 : mockKeys2 + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ keys }), + } + }) + + const url = 'https://example.com/.well-known/jwks.json' + const ttl = 0.1 // 100ms + + // First fetch + const result1 = await fetchJwksFromUrl(url, ttl) + expect(result1).toEqual(mockKeys1) + + // Wait for expiry + await new Promise(resolve => setTimeout(resolve, 150)) + + // Second fetch (should get updated keys) + const result2 = await fetchJwksFromUrl(url, ttl) + expect(result2).toEqual(mockKeys2) + }) + }) + + describe('Size Limit Enforcement', () => { + test('should reject response exceeding 100KB', async () => { + // Create a response larger than 100KB + const largeKeys = Array(1000) + .fill(null) + .map((_, i) => ({ + kid: `key-${i}`, + kty: 'RSA', + n: 'a'.repeat(300), // Make each key large + e: 'AQAB', + })) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: largeKeys }), + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('JWKS response exceeds size limit (100KB)') + }) + + test('should accept response under 100KB', async () => { + // Create a reasonable-sized response + const keys = Array(10) + .fill(null) + .map((_, i) => ({ + kid: `key-${i}`, + kty: 'RSA', + n: 'abc123', + e: 'AQAB', + })) + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys }), + }) + + const result = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(result).toHaveLength(10) + }) + + test('should accept response at exactly 100KB', async () => { + // Create a response that's exactly at the limit + const exactSize = '{ "keys": [' + 'x'.repeat(100 * 1024 - 13) + '] }' + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => exactSize, + }) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow() // Will fail JSON parse, but not size check + }) + }) + + describe('RSA Key Support', () => { + test('should handle RSA keys with standard modulus', async () => { + const rsaKey = { + kid: 'rsa-key-1', + kty: 'RSA', + use: 'sig', + alg: 'RS256', + n: '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + e: 'AQAB', + } + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [rsaKey] }), + }) + + const jwks = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(jwks).toHaveLength(1) + expect(jwks[0].kty).toBe('RSA') + expect(jwks[0].kid).toBe('rsa-key-1') + }) + + test('should handle mixed EdDSA and RSA keys', async () => { + const keys = [ + { + kid: 'eddsa-key', + kty: 'OKP', + crv: 'Ed25519', + x: '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo', + }, + { + kid: 'rsa-key', + kty: 'RSA', + n: 'abc123', + e: 'AQAB', + }, + ] + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys }), + }) + + const jwks = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(jwks).toHaveLength(2) + expect(jwks[0].kty).toBe('OKP') + expect(jwks[1].kty).toBe('RSA') + }) + }) + + describe('Real-World OIDC Provider Patterns', () => { + test('should handle Auth0-style JWKS', async () => { + const auth0Keys = { + keys: [ + { + alg: 'RS256', + kty: 'RSA', + use: 'sig', + n: 'xjlCRBqkQf4w', + e: 'AQAB', + kid: 'ABC123', + x5t: 'xyz', + x5c: ['cert-data'], + }, + ], + } + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify(auth0Keys), + }) + + const result = await fetchJwksFromUrl( + 'https://tenant.auth0.com/.well-known/jwks.json' + ) + + expect(result).toHaveLength(1) + expect(result[0].alg).toBe('RS256') + }) + + test('should handle Okta-style JWKS', async () => { + const oktaKeys = { + keys: [ + { + kty: 'RSA', + alg: 'RS256', + kid: 'key-id-1', + use: 'sig', + e: 'AQAB', + n: 'modulus-data', + }, + ], + } + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify(oktaKeys), + }) + + const result = await fetchJwksFromUrl( + 'https://domain.okta.com/oauth2/default/v1/keys' + ) + + expect(result).toHaveLength(1) + expect(result[0].use).toBe('sig') + }) + + test('should handle Google-style JWKS', async () => { + const googleKeys = { + keys: [ + { + kid: 'google-key-id', + kty: 'RSA', + alg: 'RS256', + use: 'sig', + n: 'modulus', + e: 'AQAB', + }, + ], + } + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify(googleKeys), + }) + + const result = await fetchJwksFromUrl( + 'https://www.googleapis.com/oauth2/v3/certs' + ) + + expect(result).toHaveLength(1) + }) + }) + + describe('Error Handling and Edge Cases', () => { + test('should handle network errors gracefully', async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error')) + + await expect( + fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + ).rejects.toThrow('Network error') + }) + + test('should handle DNS resolution failures', async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error('getaddrinfo ENOTFOUND')) + + await expect( + fetchJwksFromUrl('https://nonexistent.example.com/.well-known/jwks.json') + ).rejects.toThrow() + }) + + test('should handle JSON with extra fields', async () => { + const keysWithExtras = { + keys: [{ kid: 'key1', kty: 'OKP' }], + extra_field: 'ignored', + metadata: { version: '1.0' }, + } + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify(keysWithExtras), + }) + + const result = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(result).toEqual([{ kid: 'key1', kty: 'OKP' }]) + }) + + test('should handle keys with missing optional fields', async () => { + const minimalKey = { + kid: 'minimal', + kty: 'OKP', + } + + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + text: async () => JSON.stringify({ keys: [minimalKey] }), + }) + + const result = await fetchJwksFromUrl('https://example.com/.well-known/jwks.json') + + expect(result).toEqual([minimalKey]) + }) + }) +}) diff --git a/packages/flarelette-jwt-ts/tests/jwks.test.ts b/packages/flarelette-jwt-ts/tests/jwks.test.ts index 5686967..5eba2b5 100644 --- a/packages/flarelette-jwt-ts/tests/jwks.test.ts +++ b/packages/flarelette-jwt-ts/tests/jwks.test.ts @@ -172,6 +172,7 @@ describe('jwks.ts', () => { kty: 'OKP', crv: 'Ed25519', x: '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo', + alg: 'EdDSA', use: 'sig', }, ]