diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..c8a6844 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,269 @@ +# Implementation Summary: Explicit Configuration API + +## Problem Statement + +The flarelette-jwt-kit library had complex environment detection that made it difficult to use in development: + +1. **Global State Dependency**: All functions read from `globalThis.__FLARELETTE_ENV` or `process.env` +2. **Miniflare Complexity**: Required `bindEnv()` middleware to mutate global state before JWT operations +3. **No Explicit Config**: Impossible to pass configuration directly as parameters +4. **Testing Pain**: Hard to write isolated tests without environment pollution + +This led to developers **bypassing the library entirely** and implementing JWT signing directly with Web Crypto API (like in bond-math). + +## Solution: Dual API Approach + +Added a new **explicit configuration API** that allows passing config objects directly, while keeping the existing environment-based API for backward compatibility. + +### Environment-Based API (Original) + +```typescript +// Requires environment variables or bindEnv() middleware +import { sign, verify } from '@chrislyons-dev/flarelette-jwt' + +const token = await sign({ sub: 'user123' }) +const payload = await verify(token) +``` + +**Use when:** + +- Production deployment with Cloudflare bindings +- Environment-based configuration is desired +- Zero configuration code needed + +### Explicit Configuration API (New) ✨ + +```typescript +// No environment variables required! +import { + signWithConfig, + verifyWithConfig, + createHS512Config, +} from '@chrislyons-dev/flarelette-jwt' + +const config = createHS512Config('secret', { + iss: 'https://gateway.example.com', + aud: 'api.example.com', +}) + +const token = await signWithConfig({ sub: 'user123' }, config) +const payload = await verifyWithConfig(token, config) +``` + +**Use when:** + +- Development environment setup +- Testing without environment pollution +- Multiple JWT configurations needed +- Explicit control over every parameter + +## What Was Added + +### 1. Core Module: `src/explicit.ts` + +**Configuration Types:** + +- `BaseJwtConfig` - Shared config (iss, aud, ttlSeconds, leeway) +- `HS512Config` - Symmetric (shared secret) configuration +- `EdDSASignConfig` - Asymmetric signing configuration +- `EdDSAVerifyConfig` - Asymmetric verification configuration + +**Core Functions:** + +- `signWithConfig()` - Sign JWT with explicit config +- `verifyWithConfig()` - Verify JWT with explicit config + +**High-Level Functions:** + +- `createTokenWithConfig()` - Convenience wrapper for signing +- `createDelegatedTokenWithConfig()` - RFC 8693 service delegation +- `checkAuthWithConfig()` - Verify + authorize with policies + +**Helper Functions:** + +- `createHS512Config()` - Build HS512 config from base64url secret +- `createEdDSASignConfig()` - Build EdDSA sign config from JWK +- `createEdDSAVerifyConfig()` - Build EdDSA verify config from JWK + +### 2. Updated Exports: `src/index.ts` + +Added exports for all new explicit API functions and types. + +### 3. Comprehensive Tests: `tests/explicit.test.ts` + +**25 tests covering:** + +- Token signing with explicit config +- Token verification with explicit config +- Delegation patterns (RFC 8693) +- Authorization policies +- Error cases (wrong issuer, audience, secret, expired tokens) +- Isolation (no environment pollution, multiple configs) + +**All tests pass! ✓** + +### 4. Documentation + +**New Guide:** `docs/explicit-config.md` + +- Complete API reference +- Use case examples +- Migration guide +- Best practices + +**Updated:** `README.md` + +- Added "Two APIs: Choose Your Style" section +- Highlighted new explicit config option +- Added link to new documentation + +**Example:** `examples/explicit-config-example.ts` + +- HS512 example +- EdDSA example +- Service delegation example +- Development environment setup +- Testing example + +## Benefits + +### For Development + +**Before:** + +```typescript +// Required .env file with JWT_SECRET_NAME, JWT_ISS, JWT_AUD +// Required bindEnv() middleware +// Hard to pass different configs to different services +``` + +**After:** + +```typescript +// No .env file needed! +const config = { + alg: 'HS512' as const, + secret: new Uint8Array(32), // Simple dev secret + iss: 'http://localhost:3000', + aud: ['http://localhost:3001', 'http://localhost:3002'], +} + +const token = await signWithConfig({ sub: 'dev-user' }, config) +``` + +### For Testing + +**Before:** + +```typescript +// Tests polluted environment variables +// Hard to isolate tests +// Mock process.env needed +``` + +**After:** + +```typescript +describe('JWT tests', () => { + const testConfig = { + alg: 'HS512' as const, + secret: new Uint8Array(32), + iss: 'test-issuer', + aud: 'test-audience', + } + + it('should work without env vars', async () => { + const token = await signWithConfig({ sub: 'test' }, testConfig) + const payload = await verifyWithConfig(token, testConfig) + expect(payload?.sub).toBe('test') + }) +}) +``` + +### For Multi-Tenant Apps + +**Before:** + +```typescript +// Difficult to use different configs per tenant +// Global state made this essentially impossible +``` + +**After:** + +```typescript +const tenantConfigs = new Map() +tenantConfigs.set('tenant-a', createHS512Config(secretA, { ... })) +tenantConfigs.set('tenant-b', createHS512Config(secretB, { ... })) + +const config = tenantConfigs.get(tenantId) +const token = await signWithConfig(claims, config) +``` + +## Backward Compatibility + +✅ **100% backward compatible** + +- Existing environment-based API unchanged +- No breaking changes to existing code +- Both APIs can be used simultaneously + +## Code Quality + +- ✅ All tests pass (151 passed) +- ✅ No TypeScript errors +- ✅ Comprehensive JSDoc comments +- ✅ Complete type safety +- ✅ Zero environment dependencies in explicit API + +## Usage in flarelette-demo + +The explicit API can now be used in the gateway auth.ts: + +```typescript +import { signWithConfig, createHS512Config } from '@chrislyons-dev/flarelette-jwt' + +// Create config once at startup +const jwtConfig = createHS512Config(env.JWT_SECRET, { + iss: 'http://localhost:8787', + aud: ['http://localhost:8788', 'http://localhost:8789'], + ttlSeconds: 900, +}) + +// Use in auth endpoint +export async function mintToken(c: Context) { + // No need for bindEnv() middleware! + const token = await signWithConfig( + { + sub: 'user123', + permissions: ['read:data'], + }, + jwtConfig + ) + + return c.json({ token }) +} +``` + +## Next Steps + +1. **Update flarelette-demo** to use explicit API +2. **Consider adding EdDSA examples** with real key generation +3. **Update flarelette-hono** to support explicit config +4. **Add Python equivalent** of explicit API +5. **Publish new version** (v1.9.0) + +## Files Changed + +- ✨ **Added:** `packages/flarelette-jwt-ts/src/explicit.ts` (489 lines) +- ✨ **Added:** `packages/flarelette-jwt-ts/tests/explicit.test.ts` (470 lines) +- ✨ **Added:** `docs/explicit-config.md` (complete guide) +- ✨ **Added:** `examples/explicit-config-example.ts` (example code) +- 📝 **Updated:** `packages/flarelette-jwt-ts/src/index.ts` (added exports) +- 📝 **Updated:** `README.md` (added new API section) + +## Impact + +This change makes flarelette-jwt-kit **significantly more developer-friendly** while maintaining all production capabilities. Developers no longer need to bypass the library or struggle with environment setup complexity. + +The explicit API provides a **clear, testable, explicit alternative** to the environment-based approach, making the library suitable for a wider range of use cases. diff --git a/PYTHON_IMPLEMENTATION_SUMMARY.md b/PYTHON_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..8f159d0 --- /dev/null +++ b/PYTHON_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,252 @@ +# Python Explicit Configuration API - Implementation Summary + +## Overview + +Successfully implemented the explicit configuration API for the Python package (`flarelette-jwt-py`), providing **identical functionality** to the TypeScript implementation. + +## Changes Made + +### 1. New Module: `flarelette_jwt/explicit.py` (611 lines) + +**Configuration Types:** + +- `BaseJwtConfig` - Shared configuration (iss, aud, ttl_seconds, leeway) +- `HS512Config` - Symmetric (shared secret) configuration +- `EdDSASignConfig` - Asymmetric signing configuration +- `EdDSAVerifyConfig` - Asymmetric verification configuration +- `SignConfig` - Union type for signing +- `VerifyConfig` - Union type for verification + +**Core Functions:** + +- `sign_with_config()` - Sign JWT with explicit config +- `verify_with_config()` - Verify JWT with explicit config + +**High-Level Functions:** + +- `create_token_with_config()` - Convenience wrapper for signing +- `create_delegated_token_with_config()` - RFC 8693 service delegation +- `check_auth_with_config()` - Verify + authorize with policies + +**Helper Functions:** + +- `create_hs512_config()` - Build HS512 config from base64url secret +- `create_eddsa_sign_config()` - Build EdDSA sign config from JWK +- `create_eddsa_verify_config()` - Build EdDSA verify config from JWK + +### 2. Updated: `flarelette_jwt/__init__.py` + +Added exports for all new explicit API functions and types: + +- 8 new configuration types +- 6 new functions for explicit configuration + +### 3. Code Quality + +✅ **All linting checks pass** +✅ **All type checking passes** (mypy) +✅ **Follows Python best practices** + +## API Examples + +### HS512 Example + +```python +from flarelette_jwt import sign_with_config, verify_with_config, create_hs512_config + +# Create configuration object (no environment variables needed!) +config = create_hs512_config( + b'your-32-byte-secret-here...', + iss='https://gateway.example.com', + aud='api.example.com', + ttl_seconds=900, +) + +# Sign a token +token = await sign_with_config( + {'sub': 'user123', 'permissions': ['read:data']}, + config +) + +# Verify the token +payload = await verify_with_config(token, config) +print('User:', payload.get('sub')) +``` + +### EdDSA Example + +```python +from flarelette_jwt import ( + create_eddsa_sign_config, + create_eddsa_verify_config, + sign_with_config, + verify_with_config, +) + +# Producer configuration +sign_config = create_eddsa_sign_config( + { + 'kty': 'OKP', + 'crv': 'Ed25519', + 'd': 'private-key-d-value', + 'x': 'public-key-x-value', + }, + iss='https://gateway.example.com', + aud='api.example.com', + kid='ed25519-2025-01', +) + +# Consumer configuration +verify_config = create_eddsa_verify_config( + { + 'kty': 'OKP', + 'crv': 'Ed25519', + 'x': 'public-key-x-value', + }, + iss='https://gateway.example.com', + aud='api.example.com', +) + +# Sign and verify +token = await sign_with_config({'sub': 'user456'}, sign_config) +payload = await verify_with_config(token, verify_config) +``` + +### Service Delegation Example + +```python +from flarelette_jwt import create_delegated_token_with_config + +# Gateway creates delegated token for internal service +config = create_hs512_config( + secret, + iss='https://gateway.example.com', + aud='internal-api', +) + +auth0_payload = await verify_auth0_token(external_token) +internal_token = await create_delegated_token_with_config( + auth0_payload, + 'gateway-service', + config, +) +``` + +### Authorization Example + +```python +from flarelette_jwt import check_auth_with_config + +user = await check_auth_with_config( + token, + config, + { + 'require_all_permissions': ['read:data'], + 'require_any_permission': ['admin', 'editor'], + 'predicates': [ + lambda payload: payload.get('email', '').endswith('@example.com') + ], + }, +) + +if user: + print('Authorized user:', user['sub']) +``` + +## API Parity with TypeScript + +| Feature | TypeScript | Python | Status | +| ---------------------- | ---------- | ------ | ------------------------ | +| HS512 signing | ✅ | ✅ | ✅ Identical | +| HS512 verification | ✅ | ✅ | ✅ Identical | +| EdDSA verification | ✅ | ✅ | ✅ Identical | +| EdDSA signing | ✅ | ⚠️ | Runtime error (expected) | +| Service delegation | ✅ | ✅ | ✅ Identical | +| Authorization policies | ✅ | ✅ | ✅ Identical | +| Helper functions | ✅ | ✅ | ✅ Identical | +| Type safety | ✅ | ✅ | ✅ Identical | + +> **Note:** EdDSA signing in Python raises `RuntimeError` as expected - Python Workers should only verify tokens, not sign them. This matches the existing behavior. + +## Benefits for Python Users + +### Before (Environment-Based) + +```python +# Required JWT_SECRET, JWT_ISS, JWT_AUD environment variables +from flarelette_jwt import sign, verify + +token = await sign({'sub': 'user123'}) +payload = await verify(token) +``` + +### After (Explicit Config) + +```python +# No environment variables required! +from flarelette_jwt import sign_with_config, create_hs512_config + +config = create_hs512_config( + secret, + iss='http://localhost:8787', + aud='http://localhost:8788', +) + +token = await sign_with_config({'sub': 'user123'}, config) +``` + +## Testing Benefits + +```python +# Completely isolated from environment +test_config = { + 'alg': 'HS512', + 'secret': b'\\x00' * 32, + 'iss': 'test-issuer', + 'aud': 'test-audience', +} + +token = await sign_with_config({'sub': 'test'}, test_config) +payload = await verify_with_config(token, test_config) +assert payload['sub'] == 'test' +``` + +## Implementation Notes + +1. **Type Safety**: Uses TypedDict for configuration objects, providing IDE autocomplete and type checking +2. **Runtime Validation**: Validates secret length (min 32 bytes) at runtime +3. **Error Handling**: Returns `None` on verification failure (matches existing API) +4. **EdDSA Limitation**: EdDSA signing raises `RuntimeError` (intentional - Python Workers are consumers only) +5. **Compatibility**: 100% backward compatible with existing environment-based API + +## Files Changed + +- ✨ **Added:** `packages/flarelette-jwt-py/flarelette_jwt/explicit.py` (611 lines) +- 📝 **Updated:** `packages/flarelette-jwt-py/flarelette_jwt/__init__.py` (added exports) + +## Quality Checks + +✅ **Ruff linting:** All checks pass +✅ **Mypy type checking:** Success, no issues found +✅ **Import sorting:** Properly ordered +✅ **Code style:** Follows PEP 8 and project conventions + +## Next Steps + +1. ✅ TypeScript implementation complete +2. ✅ Python implementation complete +3. ⏭️ Add Python tests (mirror TypeScript test suite) +4. ⏭️ Update documentation +5. ⏭️ Publish new version + +## Cross-Language Consistency + +The Python implementation **exactly mirrors** the TypeScript implementation: + +- Same function names +- Same parameter names +- Same behavior +- Same configuration structure +- Same error handling + +This ensures developers can seamlessly work across both languages with **zero cognitive overhead**. diff --git a/README.md b/README.md index 6fabcad..fc4ff6c 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,48 @@ pip install flarelette-jwt > **Note:** The Python package requires Cloudflare Workers Python runtime (Pyodide). For standard Python environments, use the TypeScript package via Node.js. -### Basic Example +### Two APIs: Choose Your Style + +**Option 1: Environment-Based (Production-Ready)** + +Perfect for production with Cloudflare bindings. Zero configuration code: + +**TypeScript:** + +```typescript +import { sign, verify } from '@chrislyons-dev/flarelette-jwt' + +// Reads JWT_SECRET_NAME, JWT_ISS, JWT_AUD from environment +const token = await sign({ sub: 'user123', permissions: ['read:data'] }) +const payload = await verify(token) +``` + +**Option 2: Explicit Configuration (Development-Friendly)** 🆕 + +Perfect for development and testing. No environment setup required: + +**TypeScript:** + +```typescript +import { + signWithConfig, + verifyWithConfig, + createHS512Config, +} from '@chrislyons-dev/flarelette-jwt' + +// Pass configuration directly +const config = createHS512Config('your-secret', { + iss: 'https://gateway.example.com', + aud: 'api.example.com', +}) + +const token = await signWithConfig({ sub: 'user123' }, config) +const payload = await verifyWithConfig(token, config) +``` + +> **New in v1.9.0:** The explicit configuration API eliminates environment setup complexity. See [Explicit Configuration Guide](./docs/explicit-config.md). + +### Basic Example (Environment-Based) **TypeScript:** @@ -124,6 +165,7 @@ JWT_JWKS_SERVICE_NAME=GATEWAY_BINDING ## Documentation - **[Getting Started](./docs/getting-started.md)** — Installation, first token, and basic setup +- **[Explicit Configuration](./docs/explicit-config.md)** 🆕 — No environment setup required! Use config objects directly - **[Core Concepts](./docs/core-concepts.md)** — Algorithms, modes, and architecture - **[Usage Guide](./docs/usage-guide.md)** — Complete API reference for TypeScript and Python - **[Service Delegation](./docs/service-delegation.md)** — RFC 8693 actor claims for zero-trust diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index a4aef7e..7425b96 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.9.0 -> .\packages\flarelette-jwt-ts +└─┬ @chrislyons-dev/flarelette-jwt@1.10.1 -> .\packages\flarelette-jwt-ts │ Environment-driven JWT authentication for Cloudflare Workers with secret-name indirection └── jose@5.10.0 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-11-02 +**Last generated**: 2025-11-04 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 94dd57c..9475b04 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,7 +1,7 @@ # 🏗️ flarelette-jwt-kit **Architecture Documentation** -Generated 2025-11-02 07:09:15 +Generated 2025-11-04 00:58:21 ## Overview diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt.md b/docs/architecture/chrislyons_dev_flarelette_jwt.md index 54a4dc0..e1fac41 100644 --- a/docs/architecture/chrislyons_dev_flarelette_jwt.md +++ b/docs/architecture/chrislyons_dev_flarelette_jwt.md @@ -70,6 +70,17 @@ It supports custom claims and configuration overrides. View → +explicit +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. +View → + + util module High-level JWT utilities for creating, delegating, verifying, and authorizing JWT tokens | JSON Web Key Set (JWKS) utilities. diff --git a/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md b/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md new file mode 100644 index 0000000..167c91e --- /dev/null +++ b/docs/architecture/chrislyons_dev_flarelette_jwt__explicit.md @@ -0,0 +1,339 @@ +# explicit — Code View + +[← Back to Container](./chrislyons_dev_flarelette_jwt.md) | [← Back to System](./README.md) + +--- + +## Component Information + + + + + + + + + + + + + + + + + + + + +
Componentexplicit
Container@chrislyons-dev/flarelette-jwt
Typemodule
DescriptionExplicit 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.
+ +--- + +## Code Structure + +### Class Diagram + +![Class Diagram](./diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png) + +### Code Elements + +
+8 code element(s) + + + +#### Functions + +##### `signWithConfig()` + +Sign a JWT token with explicit configuration + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Signed JWT token string
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:102
+ +**Parameters:** + +- `payload`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload — - Claims to include in the token- `config`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").SignConfig — - Explicit JWT configuration- `overrides`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional per-call overrides for iss, aud, ttlSeconds +**Examples:** +```typescript + +``` + +--- +##### `verifyWithConfig()` + +Verify a JWT token with explicit configuration + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Payload if valid, null if invalid
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:160
+ +**Parameters:** + +- `token`: string — - JWT token string to verify- `config`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").VerifyConfig — - Explicit JWT configuration- `overrides`: Partial<{ iss: string; aud: string | string[]; leeway: number; }> — - Optional per-call overrides for iss, aud, leeway +**Examples:** +```typescript + +``` + +--- +##### `createTokenWithConfig()` + +Create a signed JWT token with explicit configuration + +Higher-level wrapper around signWithConfig for convenience. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Signed JWT token string
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:209
+ +**Parameters:** + +- `claims`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload — - Claims to include in the token- `config`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").SignConfig — - Explicit JWT configuration- `overrides`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional per-call overrides + +--- +##### `createDelegatedTokenWithConfig()` + +Create a delegated JWT token with explicit configuration + +Implements RFC 8693 actor claim pattern for service-to-service delegation. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — Signed JWT token string with delegation claim
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:246
+ +**Parameters:** + +- `originalPayload`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types").JwtPayload — - The verified JWT payload from external auth- `actorService`: string — - Identifier of the service creating this delegated token- `config`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").SignConfig — - Explicit JWT configuration- `overrides`: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }> — - Optional per-call overrides +**Examples:** +```typescript + +``` + +--- +##### `checkAuthWithConfig()` + +Verify and authorize a JWT token with explicit configuration + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
AsyncYes
ReturnsPromise — AuthUser if valid and authorized, null otherwise
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:332
+ +**Parameters:** + +- `token`: string — - JWT token string to verify- `config`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").VerifyConfig — - Explicit JWT configuration- `authzOpts`: import("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").AuthzOptsWithConfig — - Authorization policy requirements- `verifyOverrides`: Partial<{ iss: string; aud: string | string[]; leeway: number; }> — - Optional per-call verification overrides +**Examples:** +```typescript + +``` + +--- +##### `createHS512Config()` + +Helper function to create HS512 config from base64url-encoded secret + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsimport("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").HS512Config — HS512 configuration
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:386
+ +**Parameters:** + +- `secret`: string — - Base64url-encoded secret string- `baseConfig`: Omit & Partial> — - Base JWT configuration + +--- +##### `createEdDSASignConfig()` + +Helper function to create EdDSA sign config from JWK + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsimport("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").EdDSASignConfig — EdDSA sign configuration
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:414
+ +**Parameters:** + +- `privateJwk`: any — - Private JWK object or JSON string- `baseConfig`: Omit & Partial> — - Base JWT configuration- `kid`: string — - Optional key ID + +--- +##### `createEdDSAVerifyConfig()` + +Helper function to create EdDSA verify config from JWK + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibilitypublic
Returnsimport("C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit").EdDSAVerifyConfig — EdDSA verify configuration
LocationC:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts:436
+ +**Parameters:** + +- `publicJwk`: any — - Public JWK object or JSON string- `baseConfig`: Omit & Partial> — - Base JWT configuration + +--- + +
+ +--- + +
+← Back to Container | ← Back to System | Generated with Archlette +
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 724500c..9ccddb4 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 - 32("
adapters.bindEnv
[Component: function]
Store both environment
variables and service
bindings globally
") - style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 33("
adapters.getServiceBinding
[Component: function]
Get service binding by name
from global storage
") - style 33 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 34("
adapters.makeKit
[Component: function]
Returns a namespaced kit
whose calls use the provided
env bag. Automatically
injects JWKS service binding
if configured.
") - style 34 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 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 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 f3dd8ea..9bee11b 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,23 +7,23 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 10("
core.getCommon
[Component: function]
Get common JWT configuration
from environment Returns
partial JwtProfile-compatible
configuration
") + 10("
core.envMode
[Component: function]
") style 10 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 11("
core.getProfile
[Component: function]
Get JWT profile from
environment Returns complete
JwtProfile with detected
algorithm
") + 11("
core.getCommon
[Component: function]
Get common JWT configuration
from environment Returns
partial JwtProfile-compatible
configuration
") style 11 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 12("
core.getHSSecret
[Component: function]
") + 12("
core.getProfile
[Component: function]
Get JWT profile from
environment Returns complete
JwtProfile with detected
algorithm
") style 12 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 13("
core.getPrivateJwkString
[Component: function]
") + 13("
core.getHSSecret
[Component: function]
") style 13 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 14("
core.getPublicJwkString
[Component: function]
") + 14("
core.getPrivateJwkString
[Component: function]
") style 14 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 15("
core.getJwksServiceName
[Component: function]
") + 15("
core.getPublicJwkString
[Component: function]
") style 15 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 27("
core.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm
") - style 27 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 8("
core.envRead
[Component: function]
") - style 8 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 9("
core.envMode
[Component: function]
") + 16("
core.getJwksServiceName
[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 end 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 new file mode 100644 index 0000000..8e2a914 --- /dev/null +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.mmd @@ -0,0 +1,28 @@ +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 + + 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
") + style 21 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 22("
explicit.createHS512Config
[Component: function]
Helper function to create
HS512 config from
base64url-encoded secret
") + style 22 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 23("
explicit.createEdDSASignConfig
[Component: function]
Helper function to create
EdDSA sign config from JWK
") + style 23 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 24("
explicit.createEdDSAVerifyConfig
[Component: function]
Helper function to create
EdDSA verify config from JWK
") + style 24 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 b54afe3..87ad6a6 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,36 @@ graph TB subgraph 2 ["@chrislyons-dev/flarelette-jwt"] style 2 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 16("
util.createToken
[Component: function]
Create a signed JWT token
with optional claims
") - style 16 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 17("
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 17 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 18("
util.checkAuth
[Component: function]
Verify and authorize a JWT
token with policy enforcement
") - style 18 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 19("
util.policy
[Component: function]
Fluent builder for creating
authorization policies
") - style 19 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 20("
util.clearJwksCache
[Component: function]
Clear the JWKS cache (for
testing purposes)
") - style 20 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 21("
util.fetchJwksFromService
[Component: function]
Fetch JWKS from a service
binding Implements 5-minute
caching to reduce load on
JWKS service
") - style 21 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 22("
util.getKeyFromJwks
[Component: function]
Find and import a specific
key from JWKS by kid
") - style 22 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 23("
util.allowedThumbprints
[Component: function]
Get allowed thumbprints for
key pinning (optional
security measure)
") - style 23 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 24("
util.main
[Component: function]
") - style 24 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 25("
util.generateSecret
[Component: function]
") + 25("
util.createToken
[Component: function]
Create a signed JWT token
with optional claims
") style 25 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 26("
util.isValidBase64UrlSecret
[Component: function]
") + 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 - 28("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification
") + 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.isExpiringSoon
[Component: function]
Check if JWT payload will
expire within specified
seconds
") + 29("
util.clearJwksCache
[Component: function]
Clear the JWKS cache (for
testing purposes)
") style 29 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 30("
util.mapScopesToPermissions
[Component: function]
Map OAuth scopes to
permission strings
") + 30("
util.fetchJwksFromService
[Component: function]
Fetch JWKS from a service
binding Implements 5-minute
caching to reduce load on
JWKS service
") style 30 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 31("
util.verify
[Component: function]
Verify a JWT token with HS512
or EdDSA algorithm
") + 31("
util.getKeyFromJwks
[Component: function]
Find and import a specific
key from JWKS by kid
") style 31 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 32("
util.allowedThumbprints
[Component: function]
Get allowed thumbprints for
key pinning (optional
security measure)
") + style 32 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 33("
util.main
[Component: function]
") + 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 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 75fee2f..fcb5a85 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 40 ["flarelette-jwt"] - style 40 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 49 ["flarelette-jwt"] + style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 44("
adapters.apply_env_bindings
[Component: function]
Copy a Cloudflare Worker
`env` mapping into os.environ
so the kit can read it.
") - style 44 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 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 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 new file mode 100644 index 0000000..e5cdfe2 --- /dev/null +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__explicit.mmd @@ -0,0 +1,48 @@ +graph TB + linkStyle default fill:#ffffff + + 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 + + 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.
") + 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.
") + 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.
") + 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.
") + 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.
") + style 84 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 85("
explicit.SignConfig
[Component: type]
") + style 85 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 86("
explicit.VerifyConfig
[Component: type]
") + style 86 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 26e5bad..0ce71f9 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__util.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Classes_flarelette_jwt__util.mmd @@ -4,101 +4,101 @@ graph TB subgraph diagram ["flarelette-jwt-kit - flarelette-jwt - Components"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 40 ["flarelette-jwt"] - style 40 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 49 ["flarelette-jwt"] + style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 45("
util.JwtHeader
[Component: class]
JWT token header structure.
") - style 45 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 46("
util.ActorClaim
[Component: class]
Actor claim for service
delegation (RFC 8693).
") - style 46 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 47("
util.JwtPayload
[Component: class]
JWT token payload/claims
structure.
") - style 47 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 48("
util.JwtProfile
[Component: class]
JWT Profile structure
matching
flarelette-jwt.profile.schema.json.
") - style 48 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 49("
util.JwtCommonConfig
[Component: class]
Common JWT configuration from
environment variables.
") - style 49 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 50("
util.mode
[Component: function]
Detect JWT algorithm mode
from environment variables
based on role.
") - style 50 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 51("
util.common
[Component: function]
Get common JWT configuration
from environment.
") - style 51 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 52("
util.profile
[Component: function]
Get JWT profile from
environment.
") - style 52 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 53("
util._get_indirect
[Component: function]
") - style 53 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 54("
util.get_hs_secret_bytes
[Component: function]
") - style 54 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 55("
util.get_public_jwk_string
[Component: function]
") + 100("
util.Builder.roles_all
[Component: method]
") + style 100 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 101("
util.Builder.roles_any
[Component: method]
") + style 101 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 102("
util.Builder.where
[Component: method]
") + style 102 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 103("
util.Builder.build
[Component: method]
") + style 103 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 104("
util.create_token
[Component: function]
Create a signed JWT token
with optional claims.
") + 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.
") + style 105 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 106("
util.check_auth
[Component: function]
Verify and authorize a JWT
token with policy
enforcement.
") + style 106 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 107("
util.policy
[Component: function]
Fluent builder for creating
authorization policies.
") + style 107 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 108("
util.generate_secret
[Component: function]
") + style 108 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 109("
util.is_valid_base64url_secret
[Component: function]
") + style 109 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 110("
util.main
[Component: function]
") + style 110 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 111("
util._b64url
[Component: function]
") + style 111 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 112("
util.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm.
") + style 112 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 113("
util.ParsedJwt
[Component: class]
Parsed JWT token structure.
") + style 113 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 114("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification.
") + style 114 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 115("
util.is_expiring_soon
[Component: function]
Check if JWT payload will
expire within specified
seconds.
") + style 115 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 116("
util.map_scopes_to_permissions
[Component: function]
Map OAuth scopes to
permission strings.
") + style 116 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 117("
util._b64url_decode
[Component: function]
") + style 117 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 118("
util.verify
[Component: function]
Verify a JWT token with HS512
or EdDSA algorithm.
") + 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.AlgType
[Component: type]
") + 56("
util.ActorClaim
[Component: class]
Actor claim for service
delegation (RFC 8693).
") style 56 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 57("
util.JwtValue
[Component: type]
") + 57("
util.JwtPayload
[Component: class]
JWT token payload/claims
structure.
") style 57 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 58("
util.ClaimsDict
[Component: type]
") + 58("
util.JwtProfile
[Component: class]
JWT Profile structure
matching
flarelette-jwt.profile.schema.json.
") style 58 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 59("
util.AuthUser
[Component: class]
Authenticated user
information returned by
check_auth.
") + 59("
util.JwtCommonConfig
[Component: class]
Common JWT configuration from
environment variables.
") style 59 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 60("
util.PolicyBuilder
[Component: class]
Builder interface for
creating JWT authorization
policies.
") + 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.PolicyBuilder.base
[Component: method]
") + 61("
util.common
[Component: function]
Get common JWT configuration
from environment.
") style 61 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 62("
util.PolicyBuilder.need_all
[Component: method]
") + 62("
util.profile
[Component: function]
Get JWT profile from
environment.
") style 62 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 63("
util.PolicyBuilder.need_any
[Component: method]
") + 63("
util._get_indirect
[Component: function]
") style 63 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 64("
util.PolicyBuilder.roles_all
[Component: method]
") + 64("
util.get_hs_secret_bytes
[Component: function]
") style 64 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 65("
util.PolicyBuilder.roles_any
[Component: method]
") + 65("
util.get_public_jwk_string
[Component: function]
") style 65 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 66("
util.PolicyBuilder.where
[Component: method]
") + 66("
util.AlgType
[Component: type]
") style 66 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 67("
util.PolicyBuilder.build
[Component: method]
") + 67("
util.JwtValue
[Component: type]
") style 67 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 68("
util.Builder
[Component: class]
") + 68("
util.ClaimsDict
[Component: type]
") style 68 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 69("
util.Builder.base
[Component: method]
") - style 69 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 70("
util.Builder.need_all
[Component: method]
") - style 70 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 71("
util.Builder.need_any
[Component: method]
") - style 71 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 72("
util.Builder.roles_all
[Component: method]
") - style 72 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 73("
util.Builder.roles_any
[Component: method]
") - style 73 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 74("
util.Builder.where
[Component: method]
") - style 74 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 75("
util.Builder.build
[Component: method]
") - style 75 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 76("
util.create_token
[Component: function]
Create a signed JWT token
with optional claims.
") - style 76 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 77("
util.create_delegated_token
[Component: function]
Create a delegated JWT token
following RFC 8693 actor
claim pattern.
") - style 77 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 78("
util.check_auth
[Component: function]
Verify and authorize a JWT
token with policy
enforcement.
") - style 78 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 79("
util.policy
[Component: function]
Fluent builder for creating
authorization policies.
") - style 79 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 80("
util.generate_secret
[Component: function]
") - style 80 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 81("
util.is_valid_base64url_secret
[Component: function]
") - style 81 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 82("
util.main
[Component: function]
") - style 82 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 83("
util._b64url
[Component: function]
") - style 83 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 84("
util.sign
[Component: function]
Sign a JWT token with HS512
or EdDSA algorithm.
") - style 84 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 85("
util.ParsedJwt
[Component: class]
Parsed JWT token structure.
") - style 85 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 86("
util.parse
[Component: function]
Parse a JWT token into header
and payload without
verification.
") - style 86 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 87("
util.is_expiring_soon
[Component: function]
Check if JWT payload will
expire within specified
seconds.
") + 87("
util.AuthUser
[Component: class]
Authenticated user
information returned by
check_auth.
") style 87 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 88("
util.map_scopes_to_permissions
[Component: function]
Map OAuth scopes to
permission strings.
") + 88("
util.PolicyBuilder
[Component: class]
Builder interface for
creating JWT authorization
policies.
") style 88 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 89("
util._b64url_decode
[Component: function]
") + 89("
util.PolicyBuilder.base
[Component: method]
") style 89 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 90("
util.verify
[Component: function]
Verify a JWT token with HS512
or EdDSA algorithm.
") + 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]
") + style 98 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 99("
util.Builder.need_any
[Component: method]
") + style 99 fill:#85bbf0,stroke:#5d82a8,color:#000000 end end \ No newline at end of file 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 9d7d8a0..c781b3b 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 @@ -9,19 +9,21 @@ graph TB 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("
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.
") + 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("
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.
") + 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.
") style 5 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 6("
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.
") + 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("
adapters
[Component: module]
Component inferred from
directory: adapters
") + 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.
") style 7 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 8("
adapters
[Component: module]
Component inferred from
directory: adapters
") + style 8 fill:#85bbf0,stroke:#5d82a8,color:#000000 end - 4-- "
ParsedJwt | JwtPayload |
AlgType | Fetcher
" -->6 - 4-- "
envMode | getCommon |
getHSSecret |
getPublicJwkString
" -->3 - 7-- "
imports * as kit
" -->5 - 7-- "
imports getJwksServiceName
" -->3 - 7-- "
WorkerEnv | Fetcher
" -->6 + 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 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 20fd185..324fb12 100644 --- a/docs/architecture/diagrams/mermaid/structurizr-Components_flarelette_jwt.mmd +++ b/docs/architecture/diagrams/mermaid/structurizr-Components_flarelette_jwt.mmd @@ -4,15 +4,17 @@ graph TB subgraph diagram ["flarelette-jwt-kit - flarelette-jwt - Components"] style diagram fill:#ffffff,stroke:#ffffff - subgraph 40 ["flarelette-jwt"] - style 40 fill:#ffffff,stroke:#2e6295,color:#2e6295 + subgraph 49 ["flarelette-jwt"] + style 49 fill:#ffffff,stroke:#2e6295,color:#2e6295 - 41("
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 41 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 42("
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 42 fill:#85bbf0,stroke:#5d82a8,color:#000000 - 43("
flarelette_jwt
[Component: module]
Component derived from
directory: flarelette_jwt
") - style 43 fill:#85bbf0,stroke:#5d82a8,color:#000000 + 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 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 5c032e2..f4555e3 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 - 40("
flarelette-jwt
[Container: Service]
Environment-driven JWT
authentication for Cloudflare
Workers Python with
secret-name indirection
") - style 40 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 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 c49d30a..e2e04d3 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 @@ -86,6 +86,7 @@ skinparam rectangle<> { } rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" <> { + 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 rectangle "==core.getHSSecret\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetHSSecret @@ -94,7 +95,6 @@ rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" rectangle "==core.getJwksServiceName\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coregetJwksServiceName 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 - rectangle "==core.envMode\n[Component: function]" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.coreenvMode } @enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit-key.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit-key.puml new file mode 100644 index 0000000..da75bcd --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit-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__explicit.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.puml new file mode 100644 index 0000000..ba43262 --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.puml @@ -0,0 +1,92 @@ +@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<> { + 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 "==explicit.signWithConfig\n[Component: function]\n\nSign a JWT token with explicit configuration" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitsignWithConfig + rectangle "==explicit.verifyWithConfig\n[Component: function]\n\nVerify a JWT token with explicit configuration" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitverifyWithConfig + rectangle "==explicit.createTokenWithConfig\n[Component: function]\n\nCreate a signed JWT token with explicit configuration Higher-level wrapper around signWithConfig for convenience." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcreateTokenWithConfig + rectangle "==explicit.createDelegatedTokenWithConfig\n[Component: function]\n\nCreate a delegated JWT token with explicit configuration Implements RFC 8693 actor claim pattern for service-to-service delegation." <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcreateDelegatedTokenWithConfig + rectangle "==explicit.checkAuthWithConfig\n[Component: function]\n\nVerify and authorize a JWT token with explicit configuration" <> as flarelettejwtkit.chrislyonsdevflarelettejwt.explicitcheckAuthWithConfig + 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 +} + +@enduml \ No newline at end of file diff --git a/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__explicit-key.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__explicit-key.puml new file mode 100644 index 0000000..da75bcd --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__explicit-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_flarelette_jwt__explicit.puml b/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__explicit.puml new file mode 100644 index 0000000..c5c1724 --- /dev/null +++ b/docs/architecture/diagrams/plantuml/structurizr-Classes_flarelette_jwt__explicit.puml @@ -0,0 +1,172 @@ +@startuml +set separator none +title flarelette-jwt-kit - 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<> { + 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<> { + 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 "flarelette-jwt\n[Container: Service]" <> { + rectangle "==explicit.BaseJwtConfig\n[Component: class]\n\nBase JWT configuration shared by HS512 and EdDSA modes." <> as flarelettejwtkit.flarelettejwt.explicitBaseJwtConfig + rectangle "==explicit.HS512Config\n[Component: class]\n\nHS512 (HMAC-SHA512) symmetric configuration." <> as flarelettejwtkit.flarelettejwt.explicitHS512Config + rectangle "==explicit.EdDSASignConfig\n[Component: class]\n\nEdDSA (Ed25519) asymmetric configuration for signing." <> as flarelettejwtkit.flarelettejwt.explicitEdDSASignConfig + rectangle "==explicit.EdDSAVerifyConfig\n[Component: class]\n\nEdDSA (Ed25519) asymmetric configuration for verification." <> as flarelettejwtkit.flarelettejwt.explicitEdDSAVerifyConfig + rectangle "==explicit.AuthzOptsWithConfig\n[Component: class]\n\nAuthorization options for check_auth_with_config." <> as flarelettejwtkit.flarelettejwt.explicitAuthzOptsWithConfig + rectangle "==explicit.AuthUser\n[Component: class]\n\nAuthenticated user information." <> as flarelettejwtkit.flarelettejwt.explicitAuthUser + rectangle "==explicit._b64url\n[Component: function]\n\nEncode bytes to base64url without padding." <> as flarelettejwtkit.flarelettejwt.explicit_b64url + rectangle "==explicit._b64url_decode\n[Component: function]\n\nDecode base64url string (with or without padding)." <> as flarelettejwtkit.flarelettejwt.explicit_b64url_decode + rectangle "==explicit.sign_with_config\n[Component: function]\n\nSign a JWT token with explicit configuration." <> as flarelettejwtkit.flarelettejwt.explicitsign_with_config + rectangle "==explicit.verify_with_config\n[Component: function]\n\nVerify a JWT token with explicit configuration." <> as flarelettejwtkit.flarelettejwt.explicitverify_with_config + rectangle "==explicit.create_token_with_config\n[Component: function]\n\nCreate a signed JWT token with explicit configuration." <> as flarelettejwtkit.flarelettejwt.explicitcreate_token_with_config + rectangle "==explicit.create_delegated_token_with_config\n[Component: function]\n\nCreate a delegated JWT token with explicit configuration." <> as flarelettejwtkit.flarelettejwt.explicitcreate_delegated_token_with_config + rectangle "==explicit.check_auth_with_config\n[Component: function]\n\nVerify and authorize a JWT token with explicit configuration." <> as flarelettejwtkit.flarelettejwt.explicitcheck_auth_with_config + rectangle "==explicit.create_hs512_config\n[Component: function]\n\nHelper function to create HS512 config from base64url-encoded secret." <> as flarelettejwtkit.flarelettejwt.explicitcreate_hs512_config + rectangle "==explicit.create_eddsa_sign_config\n[Component: function]\n\nHelper function to create EdDSA sign config from JWK." <> as flarelettejwtkit.flarelettejwt.explicitcreate_eddsa_sign_config + rectangle "==explicit.create_eddsa_verify_config\n[Component: function]\n\nHelper function to create EdDSA verify config from JWK." <> as flarelettejwtkit.flarelettejwt.explicitcreate_eddsa_verify_config + rectangle "==explicit.SignConfig\n[Component: type]" <> as flarelettejwtkit.flarelettejwt.explicitSignConfig + rectangle "==explicit.VerifyConfig\n[Component: type]" <> as flarelettejwtkit.flarelettejwt.explicitVerifyConfig +} + +@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 ca47703..d3775d5 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,25 @@ skinparam rectangle<> { } rectangle "flarelette-jwt\n[Container: Service]" <> { + 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 + rectangle "==util.Builder.build\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderbuild + rectangle "==util.create_token\n[Component: function]\n\nCreate a signed JWT token with optional claims." <> as flarelettejwtkit.flarelettejwt.utilcreate_token + rectangle "==util.create_delegated_token\n[Component: function]\n\nCreate a delegated JWT token following RFC 8693 actor claim pattern." <> as flarelettejwtkit.flarelettejwt.utilcreate_delegated_token + rectangle "==util.check_auth\n[Component: function]\n\nVerify and authorize a JWT token with policy enforcement." <> as flarelettejwtkit.flarelettejwt.utilcheck_auth + rectangle "==util.policy\n[Component: function]\n\nFluent builder for creating authorization policies." <> as flarelettejwtkit.flarelettejwt.utilpolicy + rectangle "==util.generate_secret\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.utilgenerate_secret + rectangle "==util.is_valid_base64url_secret\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.utilis_valid_base64url_secret + rectangle "==util.main\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.utilmain + rectangle "==util._b64url\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.util_b64url + rectangle "==util.sign\n[Component: function]\n\nSign a JWT token with HS512 or EdDSA algorithm." <> as flarelettejwtkit.flarelettejwt.utilsign + rectangle "==util.ParsedJwt\n[Component: class]\n\nParsed JWT token structure." <> as flarelettejwtkit.flarelettejwt.utilParsedJwt + rectangle "==util.parse\n[Component: function]\n\nParse a JWT token into header and payload without verification." <> as flarelettejwtkit.flarelettejwt.utilparse + rectangle "==util.is_expiring_soon\n[Component: function]\n\nCheck if JWT payload will expire within specified seconds." <> as flarelettejwtkit.flarelettejwt.utilis_expiring_soon + rectangle "==util.map_scopes_to_permissions\n[Component: function]\n\nMap OAuth scopes to permission strings." <> as flarelettejwtkit.flarelettejwt.utilmap_scopes_to_permissions + rectangle "==util._b64url_decode\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.util_b64url_decode + rectangle "==util.verify\n[Component: function]\n\nVerify a JWT token with HS512 or EdDSA algorithm." <> as flarelettejwtkit.flarelettejwt.utilverify rectangle "==util.JwtHeader\n[Component: class]\n\nJWT token header structure." <> as flarelettejwtkit.flarelettejwt.utilJwtHeader rectangle "==util.ActorClaim\n[Component: class]\n\nActor claim for service delegation (RFC 8693)." <> as flarelettejwtkit.flarelettejwt.utilActorClaim rectangle "==util.JwtPayload\n[Component: class]\n\nJWT token payload/claims structure." <> as flarelettejwtkit.flarelettejwt.utilJwtPayload @@ -372,25 +391,6 @@ rectangle "flarelette-jwt\n[Container: Service]" <[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 - rectangle "==util.Builder.build\n[Component: method]" <> as flarelettejwtkit.flarelettejwt.utilBuilderbuild - rectangle "==util.create_token\n[Component: function]\n\nCreate a signed JWT token with optional claims." <> as flarelettejwtkit.flarelettejwt.utilcreate_token - rectangle "==util.create_delegated_token\n[Component: function]\n\nCreate a delegated JWT token following RFC 8693 actor claim pattern." <> as flarelettejwtkit.flarelettejwt.utilcreate_delegated_token - rectangle "==util.check_auth\n[Component: function]\n\nVerify and authorize a JWT token with policy enforcement." <> as flarelettejwtkit.flarelettejwt.utilcheck_auth - rectangle "==util.policy\n[Component: function]\n\nFluent builder for creating authorization policies." <> as flarelettejwtkit.flarelettejwt.utilpolicy - rectangle "==util.generate_secret\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.utilgenerate_secret - rectangle "==util.is_valid_base64url_secret\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.utilis_valid_base64url_secret - rectangle "==util.main\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.utilmain - rectangle "==util._b64url\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.util_b64url - rectangle "==util.sign\n[Component: function]\n\nSign a JWT token with HS512 or EdDSA algorithm." <> as flarelettejwtkit.flarelettejwt.utilsign - rectangle "==util.ParsedJwt\n[Component: class]\n\nParsed JWT token structure." <> as flarelettejwtkit.flarelettejwt.utilParsedJwt - rectangle "==util.parse\n[Component: function]\n\nParse a JWT token into header and payload without verification." <> as flarelettejwtkit.flarelettejwt.utilparse - rectangle "==util.is_expiring_soon\n[Component: function]\n\nCheck if JWT payload will expire within specified seconds." <> as flarelettejwtkit.flarelettejwt.utilis_expiring_soon - rectangle "==util.map_scopes_to_permissions\n[Component: function]\n\nMap OAuth scopes to permission strings." <> as flarelettejwtkit.flarelettejwt.utilmap_scopes_to_permissions - rectangle "==util._b64url_decode\n[Component: function]" <> as flarelettejwtkit.flarelettejwt.util_b64url_decode - rectangle "==util.verify\n[Component: function]\n\nVerify a JWT token with HS512 or EdDSA algorithm." <> as flarelettejwtkit.flarelettejwt.utilverify } @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 830a38c..264a373 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 @@ -30,6 +30,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 @@ -59,6 +66,7 @@ skinparam rectangle<> { rectangle "@chrislyons-dev/flarelette-jwt\n[Container: Service]" <> { 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 "==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 "==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 diff --git a/docs/architecture/diagrams/plantuml/structurizr-Components_flarelette_jwt.puml b/docs/architecture/diagrams/plantuml/structurizr-Components_flarelette_jwt.puml index 7fe1349..c42b2f1 100644 --- a/docs/architecture/diagrams/plantuml/structurizr-Components_flarelette_jwt.puml +++ b/docs/architecture/diagrams/plantuml/structurizr-Components_flarelette_jwt.puml @@ -23,6 +23,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 @@ -46,6 +53,7 @@ skinparam rectangle<> { rectangle "flarelette-jwt\n[Container: Service]" <> { rectangle "==adapters\n[Component: module]\n\nAdapters for Cloudflare Workers Environment This module provides utilities to adapt Cloudflare Workers environment variables for use with the Flarelette JWT library." <> as flarelettejwtkit.flarelettejwt.adapters rectangle "==util\n[Component: module]\n\nEnvironment 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." <> as flarelettejwtkit.flarelettejwt.util + 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.flarelettejwt.explicit rectangle "==flarelette_jwt\n[Component: module]\n\nComponent derived from directory: flarelette_jwt" <> as flarelettejwtkit.flarelettejwt.flarelette_jwt } 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 1c040e2..b4dee3d 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-key.png b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit-key.png new file mode 100644 index 0000000..d4d2249 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit-key.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 new file mode 100644 index 0000000..bebdbb5 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__explicit-key.png b/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__explicit-key.png new file mode 100644 index 0000000..d4d2249 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__explicit-key.png differ diff --git a/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__explicit.png b/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__explicit.png new file mode 100644 index 0000000..94521a4 Binary files /dev/null and b/docs/architecture/diagrams/structurizr-Classes_flarelette_jwt__explicit.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 b46c7f9..1894b57 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 469194a..4577ae0 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/diagrams/structurizr-Components_flarelette_jwt.png b/docs/architecture/diagrams/structurizr-Components_flarelette_jwt.png index cee867d..9c1eb2f 100644 Binary files a/docs/architecture/diagrams/structurizr-Components_flarelette_jwt.png and b/docs/architecture/diagrams/structurizr-Components_flarelette_jwt.png differ diff --git a/docs/architecture/flarelette-jwt-kit-ir.json b/docs/architecture/flarelette-jwt-kit-ir.json index 9a59a29..8f2136d 100644 --- a/docs/architecture/flarelette-jwt-kit-ir.json +++ b/docs/architecture/flarelette-jwt-kit-ir.json @@ -36,6 +36,13 @@ "name": "core", "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.", + "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.", "id": "chrislyons_dev_flarelette_jwt__util", @@ -78,6 +85,13 @@ "name": "util", "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.", + "id": "flarelette_jwt__explicit", + "containerId": "flarelette_jwt", + "name": "explicit", + "type": "module" + }, { "description": "Component derived from directory: flarelette_jwt", "id": "flarelette_jwt__flarelette_jwt", @@ -209,6 +223,304 @@ "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/config.ts", "lineNumber": 121 }, + { + "description": "Sign a JWT token with explicit configuration", + "id": "chrislyons_dev_flarelette_jwt__explicit__signwithconfig", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "signWithConfig", + "type": "function", + "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```" + ] + }, + "returnType": "Promise", + "returnDescription": "Signed JWT token string", + "parameters": [ + { + "name": "payload", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types\").JwtPayload", + "description": "- Claims to include in the token", + "optional": false + }, + { + "name": "config", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").SignConfig", + "description": "- Explicit JWT configuration", + "optional": false + }, + { + "name": "overrides", + "type": "Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }>", + "description": "- Optional per-call overrides for iss, aud, ttlSeconds", + "optional": true + } + ], + "visibility": "public", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 102 + }, + { + "description": "Verify a JWT token with explicit configuration", + "id": "chrislyons_dev_flarelette_jwt__explicit__verifywithconfig", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "verifyWithConfig", + "type": "function", + "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```" + ] + }, + "returnType": "Promise", + "returnDescription": "Payload if valid, null if invalid", + "parameters": [ + { + "name": "token", + "type": "string", + "description": "- JWT token string to verify", + "optional": false + }, + { + "name": "config", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").VerifyConfig", + "description": "- Explicit JWT configuration", + "optional": false + }, + { + "name": "overrides", + "type": "Partial<{ iss: string; aud: string | string[]; leeway: number; }>", + "description": "- Optional per-call overrides for iss, aud, leeway", + "optional": true + } + ], + "visibility": "public", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 160 + }, + { + "description": "Create a signed JWT token with explicit configuration\n\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." + }, + "returnType": "Promise", + "returnDescription": "Signed JWT token string", + "parameters": [ + { + "name": "claims", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types\").JwtPayload", + "description": "- Claims to include in the token", + "optional": false + }, + { + "name": "config", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").SignConfig", + "description": "- Explicit JWT configuration", + "optional": false + }, + { + "name": "overrides", + "type": "Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }>", + "description": "- Optional per-call overrides", + "optional": true + } + ], + "visibility": "public", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 209 + }, + { + "description": "Create a delegated JWT token with explicit configuration\n\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.", + "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```" + ] + }, + "returnType": "Promise", + "returnDescription": "Signed JWT token string with delegation claim", + "parameters": [ + { + "name": "originalPayload", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/types\").JwtPayload", + "description": "- The verified JWT payload from external auth", + "optional": false + }, + { + "name": "actorService", + "type": "string", + "description": "- Identifier of the service creating this delegated token", + "optional": false + }, + { + "name": "config", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").SignConfig", + "description": "- Explicit JWT configuration", + "optional": false + }, + { + "name": "overrides", + "type": "Partial<{ iss: string; aud: string | string[]; ttlSeconds: number; }>", + "description": "- Optional per-call overrides", + "optional": true + } + ], + "visibility": "public", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 246 + }, + { + "description": "Verify and authorize a JWT token with explicit configuration", + "id": "chrislyons_dev_flarelette_jwt__explicit__checkauthwithconfig", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "checkAuthWithConfig", + "type": "function", + "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```" + ] + }, + "returnType": "Promise", + "returnDescription": "AuthUser if valid and authorized, null otherwise", + "parameters": [ + { + "name": "token", + "type": "string", + "description": "- JWT token string to verify", + "optional": false + }, + { + "name": "config", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").VerifyConfig", + "description": "- Explicit JWT configuration", + "optional": false + }, + { + "name": "authzOpts", + "type": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").AuthzOptsWithConfig", + "description": "- Authorization policy requirements", + "optional": true + }, + { + "name": "verifyOverrides", + "type": "Partial<{ iss: string; aud: string | string[]; leeway: number; }>", + "description": "- Optional per-call verification overrides", + "optional": true + } + ], + "visibility": "public", + "isAsync": true, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 332 + }, + { + "description": "Helper function to create HS512 config from base64url-encoded secret", + "id": "chrislyons_dev_flarelette_jwt__explicit__createhs512config", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "createHS512Config", + "type": "function", + "documentation": { + "summary": "Helper function to create HS512 config from base64url-encoded secret" + }, + "returnType": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").HS512Config", + "returnDescription": "HS512 configuration", + "parameters": [ + { + "name": "secret", + "type": "string", + "description": "- Base64url-encoded secret string", + "optional": false + }, + { + "name": "baseConfig", + "type": "Omit & Partial>", + "description": "- Base JWT configuration", + "optional": false + } + ], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 386 + }, + { + "description": "Helper function to create EdDSA sign config from JWK", + "id": "chrislyons_dev_flarelette_jwt__explicit__createeddsasignconfig", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "createEdDSASignConfig", + "type": "function", + "documentation": { + "summary": "Helper function to create EdDSA sign config from JWK" + }, + "returnType": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").EdDSASignConfig", + "returnDescription": "EdDSA sign configuration", + "parameters": [ + { + "name": "privateJwk", + "description": "- Private JWK object or JSON string", + "optional": false + }, + { + "name": "baseConfig", + "type": "Omit & Partial>", + "description": "- Base JWT configuration", + "optional": false + }, + { + "name": "kid", + "type": "string", + "description": "- Optional key ID", + "optional": true + } + ], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 414 + }, + { + "description": "Helper function to create EdDSA verify config from JWK", + "id": "chrislyons_dev_flarelette_jwt__explicit__createeddsaverifyconfig", + "componentId": "chrislyons_dev_flarelette_jwt__explicit", + "name": "createEdDSAVerifyConfig", + "type": "function", + "documentation": { + "summary": "Helper function to create EdDSA verify config from JWK" + }, + "returnType": "import(\"C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit\").EdDSAVerifyConfig", + "returnDescription": "EdDSA verify configuration", + "parameters": [ + { + "name": "publicJwk", + "description": "- Public JWK object or JSON string", + "optional": false + }, + { + "name": "baseConfig", + "type": "Omit & Partial>", + "description": "- Base JWT configuration", + "optional": false + } + ], + "visibility": "public", + "isAsync": false, + "filePath": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "lineNumber": 436 + }, { "description": "Create a signed JWT token with optional claims", "id": "chrislyons_dev_flarelette_jwt__util__createtoken", @@ -801,56 +1113,537 @@ } }, { - "description": "Common JWT configuration from environment variables.", + "description": "Common JWT configuration from environment variables.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util__jwtcommonconfig", + "componentId": "flarelette_jwt__util", + "name": "JwtCommonConfig", + "type": "class", + "documentation": { + "summary": "Common JWT configuration from environment variables.", + "details": "Subset of JwtProfile containing the fields shared across all operations\n(signing, verification, policy checks). Extracted by common() function\nand merged with algorithm-specific configuration in profile()." + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 125, + "metadata": { + "language": "python", + "baseClasses": [ + "TypedDict" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "Detect JWT algorithm mode from environment variables based on role.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util__mode", + "componentId": "flarelette_jwt__util", + "name": "mode", + "type": "function", + "documentation": { + "summary": "Detect JWT algorithm mode from environment variables based on role.", + "details": "" + }, + "returnType": "AlgType", + "returnDescription": "Either \"HS512\" or \"EdDSA\"", + "parameters": [ + { + "name": "role", + "type": "str", + "description": "Either \"producer\" (signing) or \"consumer\" (verification)", + "optional": false + } + ], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 139, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "Get common JWT configuration from environment.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util__common", + "componentId": "flarelette_jwt__util", + "name": "common", + "type": "function", + "documentation": { + "summary": "Get common JWT configuration from environment.", + "details": "" + }, + "returnType": "JwtCommonConfig", + "returnDescription": "Configuration with iss, aud, leeway, ttl_seconds", + "parameters": [], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 169, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "Get JWT profile from environment.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util__profile", + "componentId": "flarelette_jwt__util", + "name": "profile", + "type": "function", + "documentation": { + "summary": "Get JWT profile from environment.", + "details": "Returns complete JwtProfile-compatible configuration with detected algorithm." + }, + "returnType": "dict[str, Any]", + "returnDescription": "dict containing alg, iss, aud, leeway_seconds, and ttl_seconds", + "parameters": [ + { + "name": "role", + "type": "str", + "description": "Either \"producer\" (signing) or \"consumer\" (verification)", + "optional": false + } + ], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 183, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util___get_indirect", + "componentId": "flarelette_jwt__util", + "name": "_get_indirect", + "type": "function", + "documentation": {}, + "returnType": "str | None", + "parameters": [ + { + "name": "name_var", + "type": "str", + "optional": false + }, + { + "name": "direct_var", + "type": "str", + "optional": false + } + ], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 206, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": false + } + }, + { + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util__get_hs_secret_bytes", + "componentId": "flarelette_jwt__util", + "name": "get_hs_secret_bytes", + "type": "function", + "documentation": {}, + "returnType": "bytes", + "parameters": [], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 213, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "tags": [ + "Code" + ], + "id": "flarelette_jwt__util__get_public_jwk_string", + "componentId": "flarelette_jwt__util", + "name": "get_public_jwk_string", + "type": "function", + "documentation": {}, + "returnType": "str | None", + "parameters": [], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 228, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "tags": [ + "Code", + "Type" + ], + "id": "flarelette_jwt__util__algtype", + "componentId": "flarelette_jwt__util", + "name": "AlgType", + "type": "type", + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 20, + "metadata": { + "language": "python", + "typeCategory": "TypeAlias", + "typeDefinition": "Literal['HS512', 'EdDSA']", + "isExported": true + } + }, + { + "tags": [ + "Code", + "Type" + ], + "id": "flarelette_jwt__util__jwtvalue", + "componentId": "flarelette_jwt__util", + "name": "JwtValue", + "type": "type", + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 26, + "metadata": { + "language": "python", + "typeCategory": "TypeAlias", + "typeDefinition": "str | int | float | bool | list[str] | dict[str, Any] | None", + "isExported": true + } + }, + { + "tags": [ + "Code", + "Type" + ], + "id": "flarelette_jwt__util__claimsdict", + "componentId": "flarelette_jwt__util", + "name": "ClaimsDict", + "type": "type", + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", + "lineNumber": 32, + "metadata": { + "language": "python", + "typeCategory": "TypeAlias", + "typeDefinition": "dict[str, JwtValue]", + "isExported": true + } + }, + { + "description": "Base JWT configuration shared by HS512 and EdDSA modes.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit__basejwtconfig", + "componentId": "flarelette_jwt__explicit", + "name": "BaseJwtConfig", + "type": "class", + "documentation": { + "summary": "Base JWT configuration shared by HS512 and EdDSA modes.", + "details": "Attributes:\n iss: Token issuer (iss claim)\n aud: Token audience (aud claim) - can be string or list\n ttl_seconds: Token lifetime in seconds (default: 900 = 15 minutes)\n leeway: Clock skew tolerance in seconds for verification (default: 90)" + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 25, + "metadata": { + "language": "python", + "baseClasses": [ + "TypedDict" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "HS512 (HMAC-SHA512) symmetric configuration.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit__hs512config", + "componentId": "flarelette_jwt__explicit", + "name": "HS512Config", + "type": "class", + "documentation": { + "summary": "HS512 (HMAC-SHA512) symmetric configuration.", + "details": "Uses a shared secret for both signing and verification.\n\nAttributes:\n alg: Must be 'HS512'\n secret: Shared secret key as bytes (minimum 32 bytes)" + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 41, + "metadata": { + "language": "python", + "baseClasses": [ + "BaseJwtConfig" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "EdDSA (Ed25519) asymmetric configuration for signing.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit__eddsasignconfig", + "componentId": "flarelette_jwt__explicit", + "name": "EdDSASignConfig", + "type": "class", + "documentation": { + "summary": "EdDSA (Ed25519) asymmetric configuration for signing.", + "details": "Uses a private key to sign tokens.\n\nAttributes:\n alg: Must be 'EdDSA'\n private_jwk: Private JWK dictionary for signing\n kid: Key ID to include in JWT header (optional)" + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 55, + "metadata": { + "language": "python", + "baseClasses": [ + "BaseJwtConfig" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "EdDSA (Ed25519) asymmetric configuration for verification.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit__eddsaverifyconfig", + "componentId": "flarelette_jwt__explicit", + "name": "EdDSAVerifyConfig", + "type": "class", + "documentation": { + "summary": "EdDSA (Ed25519) asymmetric configuration for verification.", + "details": "Uses a public key to verify tokens.\n\nAttributes:\n alg: Must be 'EdDSA'\n public_jwk: Public JWK dictionary for verification" + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 71, + "metadata": { + "language": "python", + "baseClasses": [ + "BaseJwtConfig" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "Authorization options for check_auth_with_config.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit__authzoptswithconfig", + "componentId": "flarelette_jwt__explicit", + "name": "AuthzOptsWithConfig", + "type": "class", + "documentation": { + "summary": "Authorization options for check_auth_with_config.", + "details": "Attributes:\n require_all_permissions: All permissions must be present\n require_any_permission: At least one permission must be present\n require_roles_all: All roles must be present\n require_roles_any: At least one role must be present\n predicates: Custom predicate functions that must all return True" + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 399, + "metadata": { + "language": "python", + "baseClasses": [ + "TypedDict" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "Authenticated user information.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit__authuser", + "componentId": "flarelette_jwt__explicit", + "name": "AuthUser", + "type": "class", + "documentation": { + "summary": "Authenticated user information.", + "details": "Returned when a token passes both verification and authorization.\n\nAttributes:\n sub: Subject identifier\n permissions: List of permission strings\n roles: List of role strings\n jti: JWT ID\n payload: Complete JWT payload" + }, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 417, + "metadata": { + "language": "python", + "baseClasses": [ + "TypedDict" + ], + "decorators": [], + "decoratorDetails": [], + "isExported": true + } + }, + { + "description": "Encode bytes to base64url without padding.", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit___b64url", + "componentId": "flarelette_jwt__explicit", + "name": "_b64url", + "type": "function", + "documentation": { + "summary": "Encode bytes to base64url without padding." + }, + "returnType": "str", + "parameters": [ + { + "name": "b", + "type": "bytes", + "optional": false + } + ], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 90, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": false + } + }, + { + "description": "Decode base64url string (with or without padding).", + "tags": [ + "Code" + ], + "id": "flarelette_jwt__explicit___b64url_decode", + "componentId": "flarelette_jwt__explicit", + "name": "_b64url_decode", + "type": "function", + "documentation": { + "summary": "Decode base64url string (with or without padding)." + }, + "returnType": "bytes", + "parameters": [ + { + "name": "s", + "type": "str", + "optional": false + } + ], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 95, + "metadata": { + "language": "python", + "decorators": [], + "decoratorDetails": [], + "isExported": false + } + }, + { + "description": "Sign a JWT token with explicit configuration.", "tags": [ "Code" ], - "id": "flarelette_jwt__util__jwtcommonconfig", - "componentId": "flarelette_jwt__util", - "name": "JwtCommonConfig", - "type": "class", + "id": "flarelette_jwt__explicit__sign_with_config", + "componentId": "flarelette_jwt__explicit", + "name": "sign_with_config", + "type": "function", "documentation": { - "summary": "Common JWT configuration from environment variables.", - "details": "Subset of JwtProfile containing the fields shared across all operations\n(signing, verification, policy checks). Extracted by common() function\nand merged with algorithm-specific configuration in profile()." + "summary": "Sign a JWT token with explicit configuration.", + "details": "", + "examples": [ + "HS512 mode:\n >>> config: HS512Config = {\n ... 'alg': 'HS512',\n ... 'secret': b'your-32-byte-secret-here...',\n ... 'iss': 'https://gateway.example.com',\n ... 'aud': 'api.example.com',\n ... 'ttl_seconds': 900\n ... }\n >>> token = await sign_with_config({'sub': 'user123'}, config)\n\n EdDSA mode:\n >>> config: EdDSASignConfig = {\n ... 'alg': 'EdDSA',\n ... 'private_jwk': {'kty': 'OKP', 'crv': 'Ed25519', 'd': '...', 'x': '...'},\n ... 'kid': 'ed25519-2025-01',\n ... 'iss': 'https://gateway.example.com',\n ... 'aud': 'api.example.com'\n ... }\n >>> token = await sign_with_config({'sub': 'user123'}, config)" + ] }, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 125, + "returnType": "str", + "returnDescription": "Signed JWT token string", + "parameters": [ + { + "name": "payload", + "type": "JwtPayload", + "description": "Claims to include in the token", + "optional": false + }, + { + "name": "config", + "type": "SignConfig", + "description": "Explicit JWT configuration", + "optional": false + } + ], + "isAsync": true, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 100, "metadata": { "language": "python", - "baseClasses": [ - "TypedDict" - ], "decorators": [], "decoratorDetails": [], "isExported": true } }, { - "description": "Detect JWT algorithm mode from environment variables based on role.", + "description": "Verify a JWT token with explicit configuration.", "tags": [ "Code" ], - "id": "flarelette_jwt__util__mode", - "componentId": "flarelette_jwt__util", - "name": "mode", + "id": "flarelette_jwt__explicit__verify_with_config", + "componentId": "flarelette_jwt__explicit", + "name": "verify_with_config", "type": "function", "documentation": { - "summary": "Detect JWT algorithm mode from environment variables based on role.", - "details": "" + "summary": "Verify a JWT token with explicit configuration.", + "details": "", + "examples": [ + "HS512 mode:\n >>> config: HS512Config = {\n ... 'alg': 'HS512',\n ... 'secret': b'your-32-byte-secret-here...',\n ... 'iss': 'https://gateway.example.com',\n ... 'aud': 'api.example.com'\n ... }\n >>> payload = await verify_with_config(token, config)\n\n EdDSA mode:\n >>> config: EdDSAVerifyConfig = {\n ... 'alg': 'EdDSA',\n ... 'public_jwk': {'kty': 'OKP', 'crv': 'Ed25519', 'x': '...'},\n ... 'iss': 'https://gateway.example.com',\n ... 'aud': 'api.example.com'\n ... }\n >>> payload = await verify_with_config(token, config)" + ] }, - "returnType": "AlgType", - "returnDescription": "Either \"HS512\" or \"EdDSA\"", + "returnType": "JwtPayload | None", + "returnDescription": "Payload if valid, None if invalid", "parameters": [ { - "name": "role", + "name": "token", "type": "str", - "description": "Either \"producer\" (signing) or \"consumer\" (verification)", + "description": "JWT token string to verify", + "optional": false + }, + { + "name": "config", + "type": "VerifyConfig", + "description": "Explicit JWT configuration", "optional": false } ], - "isAsync": false, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 139, + "isAsync": true, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 187, "metadata": { "language": "python", "decorators": [], @@ -859,24 +1652,37 @@ } }, { - "description": "Get common JWT configuration from environment.", + "description": "Create a signed JWT token with explicit configuration.", "tags": [ "Code" ], - "id": "flarelette_jwt__util__common", - "componentId": "flarelette_jwt__util", - "name": "common", + "id": "flarelette_jwt__explicit__create_token_with_config", + "componentId": "flarelette_jwt__explicit", + "name": "create_token_with_config", "type": "function", "documentation": { - "summary": "Get common JWT configuration from environment.", - "details": "" + "summary": "Create a signed JWT token with explicit configuration.", + "details": "Higher-level wrapper around sign_with_config for convenience." }, - "returnType": "JwtCommonConfig", - "returnDescription": "Configuration with iss, aud, leeway, ttl_seconds", - "parameters": [], - "isAsync": false, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 169, + "returnType": "str", + "returnDescription": "Signed JWT token string", + "parameters": [ + { + "name": "claims", + "type": "JwtPayload", + "description": "Claims to include in the token", + "optional": false + }, + { + "name": "config", + "type": "SignConfig", + "description": "Explicit JWT configuration", + "optional": false + } + ], + "isAsync": true, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 296, "metadata": { "language": "python", "decorators": [], @@ -885,31 +1691,46 @@ } }, { - "description": "Get JWT profile from environment.", + "description": "Create a delegated JWT token with explicit configuration.", "tags": [ "Code" ], - "id": "flarelette_jwt__util__profile", - "componentId": "flarelette_jwt__util", - "name": "profile", + "id": "flarelette_jwt__explicit__create_delegated_token_with_config", + "componentId": "flarelette_jwt__explicit", + "name": "create_delegated_token_with_config", "type": "function", "documentation": { - "summary": "Get JWT profile from environment.", - "details": "Returns complete JwtProfile-compatible configuration with detected algorithm." + "summary": "Create a delegated JWT token with explicit configuration.", + "details": "Implements RFC 8693 actor claim pattern for service-to-service delegation.", + "examples": [ + ">>> config: HS512Config = {\n ... 'alg': 'HS512',\n ... 'secret': b'my-secret...',\n ... 'iss': 'https://gateway.example.com',\n ... 'aud': 'internal-api'\n ... }\n >>> # Gateway receives Auth0 token and creates delegated token\n >>> auth0_payload = await verify_auth0_token(external_token)\n >>> internal_token = await create_delegated_token_with_config(\n ... auth0_payload,\n ... 'gateway-service',\n ... config\n ... )" + ] }, - "returnType": "dict[str, Any]", - "returnDescription": "dict containing alg, iss, aud, leeway_seconds, and ttl_seconds", + "returnType": "str", + "returnDescription": "Signed JWT token string with delegation claim", "parameters": [ { - "name": "role", + "name": "original_payload", + "type": "JwtPayload", + "description": "The verified JWT payload from external auth", + "optional": false + }, + { + "name": "actor_service", "type": "str", - "description": "Either \"producer\" (signing) or \"consumer\" (verification)", + "description": "Identifier of the service creating this delegated token", + "optional": false + }, + { + "name": "config", + "type": "SignConfig", + "description": "Explicit JWT configuration", "optional": false } ], - "isAsync": false, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 183, + "isAsync": true, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 323, "metadata": { "language": "python", "decorators": [], @@ -918,51 +1739,80 @@ } }, { + "description": "Verify and authorize a JWT token with explicit configuration.", "tags": [ "Code" ], - "id": "flarelette_jwt__util___get_indirect", - "componentId": "flarelette_jwt__util", - "name": "_get_indirect", + "id": "flarelette_jwt__explicit__check_auth_with_config", + "componentId": "flarelette_jwt__explicit", + "name": "check_auth_with_config", "type": "function", - "documentation": {}, - "returnType": "str | None", + "documentation": { + "summary": "Verify and authorize a JWT token with explicit configuration.", + "details": "", + "examples": [ + ">>> config: HS512Config = {\n ... 'alg': 'HS512',\n ... 'secret': b'my-secret...',\n ... 'iss': 'https://gateway.example.com',\n ... 'aud': 'api.example.com'\n ... }\n >>> user = await check_auth_with_config(token, config, {\n ... 'require_all_permissions': ['read:data'],\n ... 'require_any_permission': ['admin', 'editor']\n ... })\n >>> if user:\n ... print('Authorized user:', user['sub'])" + ] + }, + "returnType": "AuthUser | None", + "returnDescription": "AuthUser if valid and authorized, None otherwise", "parameters": [ { - "name": "name_var", + "name": "token", "type": "str", + "description": "JWT token string to verify", "optional": false }, { - "name": "direct_var", - "type": "str", + "name": "config", + "type": "VerifyConfig", + "description": "Explicit JWT configuration", "optional": false + }, + { + "name": "authz_opts", + "type": "AuthzOptsWithConfig | None", + "description": "Authorization policy requirements", + "optional": true, + "defaultValue": "None" } ], - "isAsync": false, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 206, + "isAsync": true, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 437, "metadata": { "language": "python", "decorators": [], "decoratorDetails": [], - "isExported": false + "isExported": true } }, { + "description": "Helper function to create HS512 config from base64url-encoded secret.", "tags": [ "Code" ], - "id": "flarelette_jwt__util__get_hs_secret_bytes", - "componentId": "flarelette_jwt__util", - "name": "get_hs_secret_bytes", + "id": "flarelette_jwt__explicit__create_hs512_config", + "componentId": "flarelette_jwt__explicit", + "name": "create_hs512_config", "type": "function", - "documentation": {}, - "returnType": "bytes", - "parameters": [], + "documentation": { + "summary": "Helper function to create HS512 config from base64url-encoded secret.", + "details": "" + }, + "returnType": "HS512Config", + "returnDescription": "HS512Config", + "parameters": [ + { + "name": "secret", + "type": "str | bytes", + "description": "Base64url-encoded secret string or raw bytes (minimum 32 bytes)", + "optional": false + } + ], "isAsync": false, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 213, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 519, "metadata": { "language": "python", "decorators": [], @@ -971,19 +1821,31 @@ } }, { + "description": "Helper function to create EdDSA sign config from JWK.", "tags": [ "Code" ], - "id": "flarelette_jwt__util__get_public_jwk_string", - "componentId": "flarelette_jwt__util", - "name": "get_public_jwk_string", + "id": "flarelette_jwt__explicit__create_eddsa_sign_config", + "componentId": "flarelette_jwt__explicit", + "name": "create_eddsa_sign_config", "type": "function", - "documentation": {}, - "returnType": "str | None", - "parameters": [], + "documentation": { + "summary": "Helper function to create EdDSA sign config from JWK.", + "details": "" + }, + "returnType": "EdDSASignConfig", + "returnDescription": "EdDSASignConfig", + "parameters": [ + { + "name": "private_jwk", + "type": "dict[str, Any] | str", + "description": "Private JWK dictionary or JSON string", + "optional": false + } + ], "isAsync": false, - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 228, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 561, "metadata": { "language": "python", "decorators": [], @@ -992,20 +1854,35 @@ } }, { + "description": "Helper function to create EdDSA verify config from JWK.", "tags": [ - "Code", - "Type" + "Code" ], - "id": "flarelette_jwt__util__algtype", - "componentId": "flarelette_jwt__util", - "name": "AlgType", - "type": "type", - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 20, + "id": "flarelette_jwt__explicit__create_eddsa_verify_config", + "componentId": "flarelette_jwt__explicit", + "name": "create_eddsa_verify_config", + "type": "function", + "documentation": { + "summary": "Helper function to create EdDSA verify config from JWK.", + "details": "" + }, + "returnType": "EdDSAVerifyConfig", + "returnDescription": "EdDSAVerifyConfig", + "parameters": [ + { + "name": "public_jwk", + "type": "dict[str, Any] | str", + "description": "Public JWK dictionary or JSON string", + "optional": false + } + ], + "isAsync": false, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 596, "metadata": { "language": "python", - "typeCategory": "TypeAlias", - "typeDefinition": "Literal['HS512', 'EdDSA']", + "decorators": [], + "decoratorDetails": [], "isExported": true } }, @@ -1014,16 +1891,16 @@ "Code", "Type" ], - "id": "flarelette_jwt__util__jwtvalue", - "componentId": "flarelette_jwt__util", - "name": "JwtValue", + "id": "flarelette_jwt__explicit__signconfig", + "componentId": "flarelette_jwt__explicit", + "name": "SignConfig", "type": "type", - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 26, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 86, "metadata": { "language": "python", "typeCategory": "TypeAlias", - "typeDefinition": "str | int | float | bool | list[str] | dict[str, Any] | None", + "typeDefinition": "HS512Config | EdDSASignConfig", "isExported": true } }, @@ -1032,16 +1909,16 @@ "Code", "Type" ], - "id": "flarelette_jwt__util__claimsdict", - "componentId": "flarelette_jwt__util", - "name": "ClaimsDict", + "id": "flarelette_jwt__explicit__verifyconfig", + "componentId": "flarelette_jwt__explicit", + "name": "VerifyConfig", "type": "type", - "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\env.py", - "lineNumber": 32, + "filePath": "C:\\Users\\chris\\git\\flarelette-jwt-kit\\packages\\flarelette-jwt-py\\flarelette_jwt\\explicit.py", + "lineNumber": 87, "metadata": { "language": "python", "typeCategory": "TypeAlias", - "typeDefinition": "dict[str, JwtValue]", + "typeDefinition": "HS512Config | EdDSAVerifyConfig", "isExported": true } }, @@ -2028,6 +2905,18 @@ "destination": "./types.js", "stereotype": "type-import" }, + { + "description": "SignJWT | jwtVerify | importJWK | JWTVerifyResult | JWK", + "source": "chrislyons_dev_flarelette_jwt__explicit", + "destination": "jose", + "stereotype": "import" + }, + { + "description": "imports JwtPayload", + "source": "chrislyons_dev_flarelette_jwt__explicit", + "destination": "./types.js", + "stereotype": "type-import" + }, { "description": "imports sign", "source": "chrislyons_dev_flarelette_jwt__util", @@ -2124,6 +3013,36 @@ "destination": "typing", "stereotype": "import" }, + { + "description": "annotations", + "source": "flarelette_jwt__explicit", + "destination": "__future__", + "stereotype": "import" + }, + { + "description": "base64", + "source": "flarelette_jwt__explicit", + "destination": "base64", + "stereotype": "import" + }, + { + "description": "json", + "source": "flarelette_jwt__explicit", + "destination": "json", + "stereotype": "import" + }, + { + "description": "time", + "source": "flarelette_jwt__explicit", + "destination": "time", + "stereotype": "import" + }, + { + "description": "TYPE_CHECKING | Any | Literal | TypedDict", + "source": "flarelette_jwt__explicit", + "destination": "typing", + "stereotype": "import" + }, { "description": "annotations", "source": "flarelette_jwt__util", @@ -2204,6 +3123,42 @@ "destination": "./types.js:AlgType", "stereotype": "type-import" }, + { + "description": "imports SignJWT", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "jose:SignJWT", + "stereotype": "import" + }, + { + "description": "imports jwtVerify", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "jose:jwtVerify", + "stereotype": "import" + }, + { + "description": "imports importJWK", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "jose:importJWK", + "stereotype": "import" + }, + { + "description": "imports JWTVerifyResult", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "jose:JWTVerifyResult", + "stereotype": "import" + }, + { + "description": "imports JWK", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "jose:JWK", + "stereotype": "import" + }, + { + "description": "imports JwtPayload", + "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/explicit.ts", + "destination": "./types.js:JwtPayload", + "stereotype": "type-import" + }, { "description": "imports sign", "source": "C:/Users/chris/git/flarelette-jwt-kit/packages/flarelette-jwt-ts/src/high.ts", diff --git a/docs/architecture/flarelette-jwt-kit.dsl b/docs/architecture/flarelette-jwt-kit.dsl index 8097042..1cda232 100644 --- a/docs/architecture/flarelette-jwt-kit.dsl +++ b/docs/architecture/flarelette-jwt-kit.dsl @@ -19,6 +19,10 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { description "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." technology "module" } + chrislyons_dev_flarelette_jwt__explicit = component "explicit" { + description "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." + 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." technology "module" @@ -71,6 +75,46 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "function" tags "Code" } + chrislyons_dev_flarelette_jwt__explicit__signwithconfig = component "explicit.signWithConfig" { + description "Sign a JWT token with explicit configuration" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__verifywithconfig = component "explicit.verifyWithConfig" { + description "Verify a JWT token with explicit configuration" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__createtokenwithconfig = component "explicit.createTokenWithConfig" { + description "Create a signed JWT token with explicit configuration Higher-level wrapper around signWithConfig for convenience." + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__createdelegatedtokenwithconfig = component "explicit.createDelegatedTokenWithConfig" { + description "Create a delegated JWT token with explicit configuration Implements RFC 8693 actor claim pattern for service-to-service delegation." + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__checkauthwithconfig = component "explicit.checkAuthWithConfig" { + description "Verify and authorize a JWT token with explicit configuration" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__createhs512config = component "explicit.createHS512Config" { + description "Helper function to create HS512 config from base64url-encoded secret" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__createeddsasignconfig = component "explicit.createEdDSASignConfig" { + description "Helper function to create EdDSA sign config from JWK" + technology "function" + tags "Code" + } + chrislyons_dev_flarelette_jwt__explicit__createeddsaverifyconfig = component "explicit.createEdDSAVerifyConfig" { + description "Helper function to create EdDSA verify config from JWK" + technology "function" + tags "Code" + } chrislyons_dev_flarelette_jwt__util__createtoken = component "util.createToken" { description "Create a signed JWT token with optional claims" technology "function" @@ -190,6 +234,10 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { description "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." technology "module" } + flarelette_jwt__explicit = component "explicit" { + description "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." + technology "module" + } flarelette_jwt__flarelette_jwt = component "flarelette_jwt" { description "Component derived from directory: flarelette_jwt" technology "module" @@ -265,6 +313,94 @@ workspace "flarelette-jwt-kit" "JWT authentication and authorization library" { technology "type" tags "Code,Code,Type" } + flarelette_jwt__explicit__basejwtconfig = component "explicit.BaseJwtConfig" { + description "Base JWT configuration shared by HS512 and EdDSA modes." + technology "class" + tags "Code,Code" + } + flarelette_jwt__explicit__hs512config = component "explicit.HS512Config" { + description "HS512 (HMAC-SHA512) symmetric configuration." + technology "class" + tags "Code,Code" + } + flarelette_jwt__explicit__eddsasignconfig = component "explicit.EdDSASignConfig" { + description "EdDSA (Ed25519) asymmetric configuration for signing." + technology "class" + tags "Code,Code" + } + flarelette_jwt__explicit__eddsaverifyconfig = component "explicit.EdDSAVerifyConfig" { + description "EdDSA (Ed25519) asymmetric configuration for verification." + technology "class" + tags "Code,Code" + } + flarelette_jwt__explicit__authzoptswithconfig = component "explicit.AuthzOptsWithConfig" { + description "Authorization options for check_auth_with_config." + technology "class" + tags "Code,Code" + } + flarelette_jwt__explicit__authuser = component "explicit.AuthUser" { + description "Authenticated user information." + technology "class" + tags "Code,Code" + } + flarelette_jwt__explicit___b64url = component "explicit._b64url" { + description "Encode bytes to base64url without padding." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit___b64url_decode = component "explicit._b64url_decode" { + description "Decode base64url string (with or without padding)." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__sign_with_config = component "explicit.sign_with_config" { + description "Sign a JWT token with explicit configuration." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__verify_with_config = component "explicit.verify_with_config" { + description "Verify a JWT token with explicit configuration." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__create_token_with_config = component "explicit.create_token_with_config" { + description "Create a signed JWT token with explicit configuration." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__create_delegated_token_with_config = component "explicit.create_delegated_token_with_config" { + description "Create a delegated JWT token with explicit configuration." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__check_auth_with_config = component "explicit.check_auth_with_config" { + description "Verify and authorize a JWT token with explicit configuration." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__create_hs512_config = component "explicit.create_hs512_config" { + description "Helper function to create HS512 config from base64url-encoded secret." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__create_eddsa_sign_config = component "explicit.create_eddsa_sign_config" { + description "Helper function to create EdDSA sign config from JWK." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__create_eddsa_verify_config = component "explicit.create_eddsa_verify_config" { + description "Helper function to create EdDSA verify config from JWK." + technology "function" + tags "Code,Code" + } + flarelette_jwt__explicit__signconfig = component "explicit.SignConfig" { + technology "type" + tags "Code,Code,Type" + } + flarelette_jwt__explicit__verifyconfig = component "explicit.VerifyConfig" { + technology "type" + tags "Code,Code,Type" + } flarelette_jwt__util__authuser = component "util.AuthUser" { description "Authenticated user information returned by check_auth." technology "class" @@ -607,6 +743,7 @@ branding { component chrislyons_dev_flarelette_jwt "Components__chrislyons_dev_flarelette_jwt" { include chrislyons_dev_flarelette_jwt__core + include chrislyons_dev_flarelette_jwt__explicit include chrislyons_dev_flarelette_jwt__util include chrislyons_dev_flarelette_jwt__main include chrislyons_dev_flarelette_jwt__types @@ -619,6 +756,7 @@ branding { component flarelette_jwt "Components_flarelette_jwt" { include flarelette_jwt__adapters include flarelette_jwt__util + include flarelette_jwt__explicit include flarelette_jwt__flarelette_jwt exclude "element.tag==Code" autoLayout @@ -639,6 +777,19 @@ branding { } + component chrislyons_dev_flarelette_jwt "Classes_chrislyons_dev_flarelette_jwt__explicit" { + include chrislyons_dev_flarelette_jwt__explicit__signwithconfig + include chrislyons_dev_flarelette_jwt__explicit__verifywithconfig + include chrislyons_dev_flarelette_jwt__explicit__createtokenwithconfig + include chrislyons_dev_flarelette_jwt__explicit__createdelegatedtokenwithconfig + include chrislyons_dev_flarelette_jwt__explicit__checkauthwithconfig + include chrislyons_dev_flarelette_jwt__explicit__createhs512config + include chrislyons_dev_flarelette_jwt__explicit__createeddsasignconfig + include chrislyons_dev_flarelette_jwt__explicit__createeddsaverifyconfig + autoLayout + } + + component chrislyons_dev_flarelette_jwt "Classes_chrislyons_dev_flarelette_jwt__util" { include chrislyons_dev_flarelette_jwt__util__createtoken include chrislyons_dev_flarelette_jwt__util__createdelegatedtoken @@ -723,6 +874,29 @@ branding { autoLayout } + + component flarelette_jwt "Classes_flarelette_jwt__explicit" { + include flarelette_jwt__explicit__basejwtconfig + include flarelette_jwt__explicit__hs512config + include flarelette_jwt__explicit__eddsasignconfig + include flarelette_jwt__explicit__eddsaverifyconfig + include flarelette_jwt__explicit__authzoptswithconfig + include flarelette_jwt__explicit__authuser + include flarelette_jwt__explicit___b64url + include flarelette_jwt__explicit___b64url_decode + include flarelette_jwt__explicit__sign_with_config + include flarelette_jwt__explicit__verify_with_config + include flarelette_jwt__explicit__create_token_with_config + include flarelette_jwt__explicit__create_delegated_token_with_config + include flarelette_jwt__explicit__check_auth_with_config + include flarelette_jwt__explicit__create_hs512_config + include flarelette_jwt__explicit__create_eddsa_sign_config + include flarelette_jwt__explicit__create_eddsa_verify_config + include flarelette_jwt__explicit__signconfig + include flarelette_jwt__explicit__verifyconfig + autoLayout + } + } } diff --git a/docs/architecture/flarelette_jwt.md b/docs/architecture/flarelette_jwt.md index baa012b..703177b 100644 --- a/docs/architecture/flarelette_jwt.md +++ b/docs/architecture/flarelette_jwt.md @@ -74,6 +74,17 @@ It supports both symmetric (HS512) and asymmetric (EdDSA) algorithms. View → +explicit +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. +View → + + flarelette_jwt module Component derived from directory: flarelette_jwt diff --git a/docs/architecture/flarelette_jwt__explicit.md b/docs/architecture/flarelette_jwt__explicit.md new file mode 100644 index 0000000..2fcf656 --- /dev/null +++ b/docs/architecture/flarelette_jwt__explicit.md @@ -0,0 +1,536 @@ +# explicit — Code View + +[← Back to Container](./flarelette_jwt.md) | [← Back to System](./README.md) + +--- + +## Component Information + + + + + + + + + + + + + + + + + + + + +
Componentexplicit
Containerflarelette-jwt
Typemodule
DescriptionExplicit 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.
+ +--- + +## Code Structure + +### Class Diagram + +![Class Diagram](./diagrams/structurizr-Classes_chrislyons_dev_flarelette_jwt__explicit.png) +![Class Diagram](./diagrams/structurizr-Classes_flarelette_jwt__explicit.png) + +### Code Elements + +
+18 code element(s) + + +#### Classes + +##### `BaseJwtConfig` + +Base JWT configuration shared by HS512 and EdDSA modes. + + + + + + + + + + + + + + + + +
Typeclass
Visibility
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:25
+ + +--- +##### `HS512Config` + +HS512 (HMAC-SHA512) symmetric configuration. + + + + + + + + + + + + + + + + +
Typeclass
Visibility
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:41
+ + +--- +##### `EdDSASignConfig` + +EdDSA (Ed25519) asymmetric configuration for signing. + + + + + + + + + + + + + + + + +
Typeclass
Visibility
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:55
+ + +--- +##### `EdDSAVerifyConfig` + +EdDSA (Ed25519) asymmetric configuration for verification. + + + + + + + + + + + + + + + + +
Typeclass
Visibility
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:71
+ + +--- +##### `AuthzOptsWithConfig` + +Authorization options for check_auth_with_config. + + + + + + + + + + + + + + + + +
Typeclass
Visibility
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:399
+ + +--- +##### `AuthUser` + +Authenticated user information. + + + + + + + + + + + + + + + + +
Typeclass
Visibility
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:417
+ + +--- + +#### Functions + +##### `_b64url()` + +Encode bytes to base64url without padding. + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
Returnsstr
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:90
+ +**Parameters:** + +- `b`: bytes + +--- +##### `_b64url_decode()` + +Decode base64url string (with or without padding). + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
Returnsbytes
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:95
+ +**Parameters:** + +- `s`: str + +--- +##### `sign_with_config()` + +Sign a JWT token with explicit configuration. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
AsyncYes
Returnsstr — Signed JWT token string
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:100
+ +**Parameters:** + +- `payload`: JwtPayload — Claims to include in the token- `config`: SignConfig — Explicit JWT configuration +**Examples:** +```typescript + +``` + +--- +##### `verify_with_config()` + +Verify a JWT token with explicit configuration. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
AsyncYes
ReturnsJwtPayload | None — Payload if valid, None if invalid
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:187
+ +**Parameters:** + +- `token`: str — JWT token string to verify- `config`: VerifyConfig — Explicit JWT configuration +**Examples:** +```typescript + +``` + +--- +##### `create_token_with_config()` + +Create a signed JWT token with explicit configuration. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
AsyncYes
Returnsstr — Signed JWT token string
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:296
+ +**Parameters:** + +- `claims`: JwtPayload — Claims to include in the token- `config`: SignConfig — Explicit JWT configuration + +--- +##### `create_delegated_token_with_config()` + +Create a delegated JWT token with explicit configuration. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
AsyncYes
Returnsstr — Signed JWT token string with delegation claim
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:323
+ +**Parameters:** + +- `original_payload`: JwtPayload — The verified JWT payload from external auth- `actor_service`: str — Identifier of the service creating this delegated token- `config`: SignConfig — Explicit JWT configuration +**Examples:** +```typescript + +``` + +--- +##### `check_auth_with_config()` + +Verify and authorize a JWT token with explicit configuration. + + + + + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
AsyncYes
ReturnsAuthUser | None — AuthUser if valid and authorized, None otherwise
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:437
+ +**Parameters:** + +- `token`: str — JWT token string to verify- `config`: VerifyConfig — Explicit JWT configuration- `authz_opts`: AuthzOptsWithConfig | None — Authorization policy requirements +**Examples:** +```typescript + +``` + +--- +##### `create_hs512_config()` + +Helper function to create HS512 config from base64url-encoded secret. + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
ReturnsHS512Config — HS512Config
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:519
+ +**Parameters:** + +- `secret`: str | bytes — Base64url-encoded secret string or raw bytes (minimum 32 bytes) + +--- +##### `create_eddsa_sign_config()` + +Helper function to create EdDSA sign config from JWK. + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
ReturnsEdDSASignConfig — EdDSASignConfig
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:561
+ +**Parameters:** + +- `private_jwk`: dict[str, Any] | str — Private JWK dictionary or JSON string + +--- +##### `create_eddsa_verify_config()` + +Helper function to create EdDSA verify config from JWK. + + + + + + + + + + + + + + + + + + + + +
Typefunction
Visibility
ReturnsEdDSAVerifyConfig — EdDSAVerifyConfig
LocationC:\Users\chris\git\flarelette-jwt-kit\packages\flarelette-jwt-py\flarelette_jwt\explicit.py:596
+ +**Parameters:** + +- `public_jwk`: dict[str, Any] | str — Public JWK dictionary or JSON string + +--- + +
+ +--- + + diff --git a/docs/explicit-config.md b/docs/explicit-config.md new file mode 100644 index 0000000..5a6fe90 --- /dev/null +++ b/docs/explicit-config.md @@ -0,0 +1,461 @@ +# Explicit Configuration API + +> **New in v1.9.0**: Pass configuration directly without environment variables + +## Overview + +The explicit configuration API provides a way to use flarelette-jwt-kit **without relying on environment variables or global state**. This is ideal for: + +- **Development environments** where setting up `.env` files and bindings is cumbersome +- **Testing** where you need isolated, reproducible JWT configurations +- **Multi-tenant scenarios** where different tokens need different configurations +- **Debugging** when you want explicit control over every JWT parameter + +## Quick Start + +### HS512 (Symmetric) Example + +```typescript +import { + createHS512Config, + createTokenWithConfig, + verifyWithConfig, +} from '@chrislyons-dev/flarelette-jwt' + +// Create configuration object (no environment variables needed!) +const config = createHS512Config('your-base64url-secret-here', { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + ttlSeconds: 900, +}) + +// Sign a token +const token = await createTokenWithConfig( + { + sub: 'user123', + permissions: ['read:data'], + }, + config +) + +// Verify the token +const payload = await verifyWithConfig(token, config) +console.log('User:', payload?.sub) +``` + +### EdDSA (Asymmetric) Example + +```typescript +import { + createEdDSASignConfig, + createEdDSAVerifyConfig, + signWithConfig, + verifyWithConfig, +} from '@chrislyons-dev/flarelette-jwt' + +// Producer configuration (signs tokens) +const signConfig = createEdDSASignConfig( + { + kty: 'OKP', + crv: 'Ed25519', + d: 'private-key-d-value', + x: 'public-key-x-value', + }, + { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + }, + 'ed25519-2025-01' // Key ID +) + +// Consumer configuration (verifies tokens) +const verifyConfig = createEdDSAVerifyConfig( + { + kty: 'OKP', + crv: 'Ed25519', + x: 'public-key-x-value', + }, + { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + } +) + +// Sign and verify +const token = await signWithConfig({ sub: 'user456' }, signConfig) +const payload = await verifyWithConfig(token, verifyConfig) +``` + +## API Reference + +### Configuration Types + +#### `BaseJwtConfig` + +Base configuration shared by all JWT operations: + +```typescript +interface BaseJwtConfig { + iss: string // Token issuer (iss claim) + aud: string | string[] // Token audience (aud claim) + ttlSeconds?: number // Token lifetime (default: 900 = 15 min) + leeway?: number // Clock skew tolerance (default: 90 sec) +} +``` + +#### `HS512Config` + +Symmetric (shared secret) configuration: + +```typescript +interface HS512Config extends BaseJwtConfig { + alg: 'HS512' + secret: Uint8Array // Minimum 32 bytes +} +``` + +#### `EdDSASignConfig` + +Asymmetric signing configuration: + +```typescript +interface EdDSASignConfig extends BaseJwtConfig { + alg: 'EdDSA' + privateJwk: JWK // Private Ed25519 key + kid?: string // Key ID for rotation +} +``` + +#### `EdDSAVerifyConfig` + +Asymmetric verification configuration: + +```typescript +interface EdDSAVerifyConfig extends BaseJwtConfig { + alg: 'EdDSA' + publicJwk: JWK // Public Ed25519 key +} +``` + +### Core Functions + +#### `signWithConfig(payload, config, overrides?)` + +Sign a JWT token with explicit configuration. + +**Parameters:** + +- `payload: JwtPayload` - Claims to include in token +- `config: SignConfig` - HS512 or EdDSA sign configuration +- `overrides?: Partial<{ iss, aud, ttlSeconds }>` - Per-call overrides + +**Returns:** `Promise` - Signed JWT token + +#### `verifyWithConfig(token, config, overrides?)` + +Verify a JWT token with explicit configuration. + +**Parameters:** + +- `token: string` - JWT token to verify +- `config: VerifyConfig` - HS512 or EdDSA verify configuration +- `overrides?: Partial<{ iss, aud, leeway }>` - Per-call overrides + +**Returns:** `Promise` - Payload if valid, null if invalid + +### High-Level Functions + +#### `createTokenWithConfig(claims, config, overrides?)` + +Convenience wrapper around `signWithConfig()`. + +#### `createDelegatedTokenWithConfig(originalPayload, actorService, config, overrides?)` + +Create an RFC 8693 delegated token for service-to-service authentication. + +**Example:** + +```typescript +// Gateway receives Auth0 token +const auth0Payload = await verifyAuth0Token(externalToken) + +// Create delegated token for internal API +const internalToken = await createDelegatedTokenWithConfig( + auth0Payload, + 'gateway-service', // Actor identifier + config +) + +// Result includes: +// - sub: original user +// - act: { sub: 'gateway-service' } +// - permissions: original permissions (no escalation) +``` + +#### `checkAuthWithConfig(token, config, authzOpts?, verifyOverrides?)` + +Verify and authorize a token with policy enforcement. + +**Example:** + +```typescript +const user = await checkAuthWithConfig(token, config, { + require_all_permissions: ['read:data', 'write:data'], + require_any_permission: ['admin', 'editor'], + require_roles_all: ['user'], + predicates: [payload => payload.email?.endsWith('@example.com')], +}) + +if (user) { + console.log('Authorized:', user.sub, user.permissions) +} +``` + +### Helper Functions + +#### `createHS512Config(secret, baseConfig)` + +Create HS512 configuration from base64url-encoded secret. + +**Parameters:** + +- `secret: string` - Base64url-encoded secret (generates with `npx flarelette-jwt-secret`) +- `baseConfig: Omit & Partial<...>` - Base configuration + +**Returns:** `HS512Config` + +#### `createEdDSASignConfig(privateJwk, baseConfig, kid?)` + +Create EdDSA signing configuration from private JWK. + +**Parameters:** + +- `privateJwk: JWK | string` - Private JWK object or JSON string +- `baseConfig` - Base configuration +- `kid?: string` - Key ID + +**Returns:** `EdDSASignConfig` + +#### `createEdDSAVerifyConfig(publicJwk, baseConfig)` + +Create EdDSA verification configuration from public JWK. + +**Parameters:** + +- `publicJwk: JWK | string` - Public JWK object or JSON string +- `baseConfig` - Base configuration + +**Returns:** `EdDSAVerifyConfig` + +## Use Cases + +### Development Environment + +**Problem:** Setting up `.env` files and Cloudflare bindings for local development is complex. + +**Solution:** Create config objects directly: + +```typescript +// No .env files needed! +const devConfig = { + alg: 'HS512' as const, + secret: new Uint8Array(32), // Simple dev secret + iss: 'http://localhost:3000', + aud: ['http://localhost:3001', 'http://localhost:3002'], + ttlSeconds: 3600, // 1 hour +} + +// All services use the same config +const token = await createTokenWithConfig({ sub: 'dev-user' }, devConfig) +``` + +### Testing + +**Problem:** Tests need isolated JWT configurations without environment pollution. + +**Solution:** Each test gets its own config: + +```typescript +describe('JWT authentication', () => { + const testConfig = { + alg: 'HS512' as const, + secret: new Uint8Array(32), + iss: 'test-issuer', + aud: 'test-audience', + } + + it('should verify valid tokens', async () => { + const token = await createTokenWithConfig({ sub: 'test' }, testConfig) + const payload = await verifyWithConfig(token, testConfig) + expect(payload?.sub).toBe('test') + }) +}) +``` + +### Multi-Tenant Applications + +**Problem:** Different tenants need different JWT configurations. + +**Solution:** Store configurations per tenant: + +```typescript +const tenantConfigs = new Map() + +tenantConfigs.set('tenant-a', createHS512Config(secretA, { ... })) +tenantConfigs.set('tenant-b', createHS512Config(secretB, { ... })) + +// Use tenant-specific config +const config = tenantConfigs.get(tenantId) +const token = await createTokenWithConfig(claims, config) +``` + +## Comparison: Environment-Based vs Explicit + +### Environment-Based API (Original) + +```typescript +// Requires environment variables: +// JWT_SECRET_NAME=MY_JWT_SECRET +// JWT_ISS=https://gateway.example.com +// JWT_AUD=api.example.com + +import { sign, verify } from '@chrislyons-dev/flarelette-jwt' + +// Reads from environment automatically +const token = await sign({ sub: 'user123' }) +const payload = await verify(token) +``` + +**Pros:** + +- Zero configuration code +- Works great in production with Cloudflare bindings +- Automatic algorithm detection + +**Cons:** + +- Requires `.env` setup for development +- Global state makes testing harder +- Cannot use multiple configurations simultaneously + +### Explicit Configuration API (New) + +```typescript +// No environment variables required! +import { + signWithConfig, + verifyWithConfig, + createHS512Config, +} from '@chrislyons-dev/flarelette-jwt' + +const config = createHS512Config(secret, { + iss: 'https://gateway.example.com', + aud: 'api.example.com', +}) + +const token = await signWithConfig({ sub: 'user123' }, config) +const payload = await verifyWithConfig(token, config) +``` + +**Pros:** + +- No environment setup required +- Explicit and testable +- Multiple configurations in same process +- No global state + +**Cons:** + +- More verbose +- Must manage configuration objects + +## Migration Guide + +### From Environment-Based to Explicit + +**Before:** + +```typescript +import { sign, verify } from '@chrislyons-dev/flarelette-jwt' + +// .env file required: +// JWT_SECRET_NAME=MY_JWT_SECRET +// JWT_ISS=https://gateway.example.com +// JWT_AUD=api.example.com + +const token = await sign({ sub: 'user' }) +const payload = await verify(token) +``` + +**After:** + +```typescript +import { + signWithConfig, + verifyWithConfig, + createHS512Config, +} from '@chrislyons-dev/flarelette-jwt' + +const config = createHS512Config(process.env.MY_JWT_SECRET!, { + iss: 'https://gateway.example.com', + aud: 'api.example.com', +}) + +const token = await signWithConfig({ sub: 'user' }, config) +const payload = await verifyWithConfig(token, config) +``` + +### Gradual Migration + +You can use both APIs in the same project: + +```typescript +// Production: environment-based +import { sign, verify } from '@chrislyons-dev/flarelette-jwt' + +// Development: explicit config +import { signWithConfig, createHS512Config } from '@chrislyons-dev/flarelette-jwt' + +const config = + process.env.NODE_ENV === 'production' + ? null // Use environment-based API + : createHS512Config('dev-secret', { iss: '...', aud: '...' }) + +const token = config ? await signWithConfig(claims, config) : await sign(claims) +``` + +## Best Practices + +1. **Use explicit API for development and testing** - Simplifies setup +2. **Use environment-based API for production** - Leverages Cloudflare bindings +3. **Store secrets securely** - Never hardcode secrets in source code +4. **Validate configurations** - Check secret length, issuer/audience values +5. **Reuse config objects** - Create once, use many times +6. **Consider a config factory** - Abstract configuration creation + +```typescript +// Example: Config factory +function createJwtConfig(env: 'dev' | 'prod'): HS512Config { + if (env === 'dev') { + return { + alg: 'HS512', + secret: new Uint8Array(32), // Dev secret + iss: 'http://localhost:3000', + aud: 'http://localhost:3001', + ttlSeconds: 3600, + } + } + + return createHS512Config(process.env.JWT_SECRET!, { + iss: process.env.JWT_ISS!, + aud: process.env.JWT_AUD!, + }) +} +``` + +## See Also + +- [Getting Started Guide](./getting-started.md) - Basic JWT usage +- [Security Guide](./security-guide.md) - Cryptographic best practices +- [Service Delegation](./service-delegation.md) - RFC 8693 patterns +- [Cloudflare Workers](./cloudflare-workers.md) - Production deployment diff --git a/docs/quick-start-demo.md b/docs/quick-start-demo.md new file mode 100644 index 0000000..6ca49b6 --- /dev/null +++ b/docs/quick-start-demo.md @@ -0,0 +1,260 @@ +# Quick Start: Using Explicit Config in flarelette-demo + +## Problem Solved + +Previously, using flarelette-jwt in the flarelette-demo required: + +1. Setting up `.dev.vars` files +2. Using `bindEnv()` middleware to set global state +3. Complex Miniflare configurations +4. Dealing with `globalThis.__FLARELETTE_ENV` pollution + +With the new explicit configuration API, **none of this is required!** + +## Gateway Integration + +### Step 1: Create JWT Config at Startup + +In `workers/gateway/src/index.ts`: + +```typescript +import { createHS512Config, type HS512Config } from '@chrislyons-dev/flarelette-jwt' + +// Create config once at app startup +function createJwtConfig(env: Env): HS512Config { + return createHS512Config(env.JWT_SECRET, { + iss: 'http://localhost:8787', + aud: ['http://localhost:8788', 'http://localhost:8789', 'http://localhost:8790'], + ttlSeconds: 900, // 15 minutes + }) +} + +// Store config in app context +app.use('*', async (c, next) => { + c.set('jwtConfig', createJwtConfig(c.env)) + await next() +}) +``` + +### Step 2: Update Token Minting in auth.ts + +Replace the complex environment-based code: + +```typescript +// OLD - Complex environment setup +import { sign } from '@chrislyons-dev/flarelette-jwt' +import { bindEnv } from '@chrislyons-dev/flarelette-jwt/adapters' + +// Required bindEnv() middleware +app.use('*', (c, next) => { + bindEnv(c.env) + return next() +}) + +// Still reads from global state +const token = await sign({ sub: 'user123' }) +``` + +With simple, explicit code: + +```typescript +// NEW - Explicit and clean +import { signWithConfig } from '@chrislyons-dev/flarelette-jwt' + +export async function mintInternalToken(c: Context, claims: JwtPayload) { + const config = c.get('jwtConfig') // Get from context + return await signWithConfig(claims, config) +} + +// Usage in endpoint +app.post('/auth/token', async c => { + const externalToken = c.req.header('Authorization') + const externalPayload = await verifyAuth0Token(externalToken) + + // Mint internal token with explicit config + const token = await mintInternalToken(c, { + sub: externalPayload.sub, + email: externalPayload.email, + permissions: externalPayload.permissions, + }) + + return c.json({ token }) +}) +``` + +## Backend Service Integration + +### Step 3: Verify Tokens in Backend Services + +In `workers/content-service/src/index.ts`: + +```typescript +import { verifyWithConfig, createHS512Config } from '@chrislyons-dev/flarelette-jwt' + +// Create config at startup +const jwtConfig = createHS512Config(env.JWT_SECRET, { + iss: 'http://localhost:8787', + aud: 'http://localhost:8788', // This service's URL +}) + +// Middleware to verify tokens +app.use('*', async (c, next) => { + const authHeader = c.req.header('Authorization') + if (!authHeader?.startsWith('Bearer ')) { + return c.json({ error: 'Unauthorized' }, 401) + } + + const token = authHeader.slice(7) + const payload = await verifyWithConfig(token, jwtConfig) + + if (!payload) { + return c.json({ error: 'Invalid token' }, 401) + } + + c.set('user', payload) + await next() +}) +``` + +## Development Environment Setup + +### No .dev.vars Required! + +Create a shared config for all services: + +```typescript +// dev-config.ts (shared across all workers) +import { type HS512Config } from '@chrislyons-dev/flarelette-jwt' + +// Simple dev secret - same for all services +const DEV_SECRET = Buffer.alloc(32, 42) // 32 bytes, all set to 42 + +export const DEV_JWT_CONFIG: HS512Config = { + alg: 'HS512', + secret: DEV_SECRET, + iss: 'http://localhost:8787', + aud: ['http://localhost:8788', 'http://localhost:8789', 'http://localhost:8790'], + ttlSeconds: 3600, // 1 hour for dev +} +``` + +Use in all services: + +```typescript +import { signWithConfig, verifyWithConfig } from '@chrislyons-dev/flarelette-jwt' +import { DEV_JWT_CONFIG } from './dev-config' + +// Gateway +const token = await signWithConfig(claims, DEV_JWT_CONFIG) + +// Backend services +const payload = await verifyWithConfig(token, DEV_JWT_CONFIG) +``` + +## Testing + +Write tests without any environment setup: + +```typescript +import { describe, it, expect } from 'vitest' +import { signWithConfig, verifyWithConfig } from '@chrislyons-dev/flarelette-jwt' + +describe('Gateway JWT minting', () => { + const testConfig = { + alg: 'HS512' as const, + secret: new Uint8Array(32), + iss: 'test-gateway', + aud: 'test-service', + } + + it('should mint valid token', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig) + const payload = await verifyWithConfig(token, testConfig) + + expect(payload?.sub).toBe('user123') + }) +}) +``` + +## Migration Checklist + +- [ ] Update gateway to use `createHS512Config()` +- [ ] Replace `sign()` with `signWithConfig()` +- [ ] Update backend services to use `verifyWithConfig()` +- [ ] Remove `bindEnv()` middleware calls +- [ ] Simplify `.dev.vars` files (just need `JWT_SECRET` now) +- [ ] Remove custom Miniflare scripts (can use `wrangler dev` directly) +- [ ] Update tests to use explicit config +- [ ] Remove environment variable setup from tests + +## Benefits for flarelette-demo + +1. **Simpler development** - No complex environment setup +2. **Faster startup** - No need to configure bindings +3. **Better testing** - Isolated, reproducible tests +4. **Clearer code** - Explicit configuration is self-documenting +5. **Easier debugging** - No hidden global state +6. **Multiple configs** - Can test different scenarios easily + +## Example: Complete Gateway Setup + +```typescript +// workers/gateway/src/index.ts +import { Hono } from 'hono' +import { + signWithConfig, + createHS512Config, + type HS512Config, +} from '@chrislyons-dev/flarelette-jwt' + +type Env = { + JWT_SECRET: string +} + +const app = new Hono<{ Bindings: Env; Variables: { jwtConfig: HS512Config } }>() + +// Create config middleware +app.use('*', async (c, next) => { + const config = createHS512Config(c.env.JWT_SECRET, { + iss: 'http://localhost:8787', + aud: ['http://localhost:8788', 'http://localhost:8789'], + ttlSeconds: 900, + }) + c.set('jwtConfig', config) + await next() +}) + +// Auth endpoint +app.post('/auth/token', async c => { + const config = c.get('jwtConfig') + + // Verify external token (Auth0, etc.) + const externalToken = c.req.header('Authorization')?.slice(7) + const externalPayload = await verifyAuth0Token(externalToken) + + // Mint internal token + const token = await signWithConfig( + { + sub: externalPayload.sub, + email: externalPayload.email, + permissions: externalPayload.permissions, + }, + config + ) + + return c.json({ token }) +}) + +export default app +``` + +## Next Steps + +1. Install updated flarelette-jwt-kit: `npm install @chrislyons-dev/flarelette-jwt@latest` +2. Update gateway auth.ts to use explicit config +3. Update backend services to use explicit config +4. Remove bindEnv() middleware +5. Simplify development environment +6. Run tests to verify everything works + +The explicit configuration API makes flarelette-jwt-kit **as simple to use as direct Web Crypto API**, but with all the benefits of the library's features (delegation, policies, EdDSA support, etc.). diff --git a/eslint.config.js b/eslint.config.js index 9c21bf3..2bf5d26 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,7 @@ export default [ 'coverage/**', '**/htmlcov/**', '**/.pytest_cache/**', + '**/examples/**', ], }, diff --git a/examples/explicit-config-example.ts b/examples/explicit-config-example.ts new file mode 100644 index 0000000..16cdc34 --- /dev/null +++ b/examples/explicit-config-example.ts @@ -0,0 +1,259 @@ +/** + * Example: Using Explicit Configuration API + * + * This example demonstrates how to use the new explicit configuration API + * that doesn't rely on environment variables or global state. This is ideal + * for development environments, testing, and scenarios where you need + * full control over JWT configuration. + */ + +import { + signWithConfig, + verifyWithConfig, + createTokenWithConfig, + createDelegatedTokenWithConfig, + checkAuthWithConfig, + createHS512Config, + createEdDSASignConfig, + createEdDSAVerifyConfig, + type HS512Config, + type EdDSASignConfig, + type EdDSAVerifyConfig, +} from '@chrislyons-dev/flarelette-jwt' + +// ============================================================================ +// Example 1: HS512 (Symmetric) Configuration +// ============================================================================ + +async function exampleHS512() { + console.log('\n=== HS512 Example ===\n') + + // Create a base64url-encoded secret (in production, use the CLI tool) + const secretString = 'your-base64url-encoded-secret-at-least-32-bytes-long' + + // Create configuration object + const config: HS512Config = createHS512Config(secretString, { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + ttlSeconds: 900, // 15 minutes + leeway: 90, // 90 seconds clock skew tolerance + }) + + // Sign a token + const token = await signWithConfig( + { + sub: 'user123', + email: 'user@example.com', + permissions: ['read:data', 'write:data'], + }, + config + ) + + console.log('Signed token:', token.substring(0, 50) + '...') + + // Verify the token + const payload = await verifyWithConfig(token, config) + console.log('Verified payload:', payload) + + // Check authorization with policy + const user = await checkAuthWithConfig(token, config, { + require_all_permissions: ['read:data'], + }) + + if (user) { + console.log('Authorized user:', user.sub, user.permissions) + } else { + console.log('Authorization failed') + } +} + +// ============================================================================ +// Example 2: EdDSA (Asymmetric) Configuration +// ============================================================================ + +async function exampleEdDSA() { + console.log('\n=== EdDSA Example ===\n') + + // In production, generate these with: npx flarelette-jwt-keygen + const privateJwk = { + kty: 'OKP', + crv: 'Ed25519', + d: 'your-private-key-d-value', + x: 'your-public-key-x-value', + } + + const publicJwk = { + kty: 'OKP', + crv: 'Ed25519', + x: 'your-public-key-x-value', + } + + // Create sign configuration (for token producers) + const signConfig: EdDSASignConfig = createEdDSASignConfig( + privateJwk, + { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + ttlSeconds: 900, + }, + 'ed25519-2025-01' // Key ID + ) + + // Create verify configuration (for token consumers) + const verifyConfig: EdDSAVerifyConfig = createEdDSAVerifyConfig(publicJwk, { + iss: 'https://gateway.example.com', + aud: 'api.example.com', + leeway: 90, + }) + + // Sign a token + const token = await signWithConfig( + { + sub: 'user456', + email: 'user@example.com', + permissions: ['read:admin'], + }, + signConfig + ) + + console.log('Signed token:', token.substring(0, 50) + '...') + + // Verify the token + const payload = await verifyWithConfig(token, verifyConfig) + console.log('Verified payload:', payload) +} + +// ============================================================================ +// Example 3: Service Delegation (RFC 8693) +// ============================================================================ + +async function exampleDelegation() { + console.log('\n=== Service Delegation Example ===\n') + + const config: HS512Config = createHS512Config( + 'your-base64url-encoded-secret-at-least-32-bytes-long', + { + iss: 'https://gateway.example.com', + aud: 'internal-api', + ttlSeconds: 300, // 5 minutes for internal tokens + } + ) + + // Simulate external Auth0 token payload + const externalPayload = { + sub: 'auth0|user123', + email: 'user@example.com', + permissions: ['read:data', 'write:data'], + } + + // Gateway creates delegated token for internal service + const delegatedToken = await createDelegatedTokenWithConfig( + externalPayload, + 'gateway-service', // Actor service identifier + config + ) + + console.log('Delegated token created') + + // Verify the delegated token in backend service + const verifiedPayload = await verifyWithConfig(delegatedToken, config) + console.log('Delegated token payload:', { + sub: verifiedPayload?.sub, // Original user + act: verifiedPayload?.act, // Acting service + permissions: verifiedPayload?.permissions, // Original permissions + }) +} + +// ============================================================================ +// Example 4: Development Environment Setup (No .env files needed!) +// ============================================================================ + +async function exampleDevEnvironment() { + console.log('\n=== Development Environment Example ===\n') + + // Create a simple secret for local development + const devSecret = Buffer.alloc(32) // All zeros for simplicity + + const devConfig: HS512Config = { + alg: 'HS512', + secret: devSecret, + iss: 'http://localhost:3000', + aud: ['http://localhost:3001', 'http://localhost:3002'], + ttlSeconds: 3600, // 1 hour for local dev + } + + // Gateway mints token + const token = await createTokenWithConfig( + { + sub: 'dev-user', + email: 'dev@local', + permissions: ['*'], // All permissions in dev + }, + devConfig + ) + + console.log('Dev token created:', token.substring(0, 50) + '...') + + // Backend services verify with same config (no env vars needed!) + const user = await checkAuthWithConfig(token, devConfig) + console.log('Dev user:', user?.sub, user?.permissions) +} + +// ============================================================================ +// Example 5: Testing Without Environment Setup +// ============================================================================ + +async function exampleTesting() { + console.log('\n=== Testing Example ===\n') + + // Test configuration - completely isolated from environment + const testConfig: HS512Config = { + alg: 'HS512', + secret: new Uint8Array(32), // All zeros + iss: 'test-issuer', + aud: 'test-audience', + ttlSeconds: 60, + } + + // Create test token + const token = await createTokenWithConfig( + { + sub: 'test-user', + permissions: ['read:test'], + }, + testConfig + ) + + // Verify in test + const payload = await verifyWithConfig(token, testConfig) + console.assert(payload?.sub === 'test-user', 'Subject should match') + console.assert( + (payload?.permissions as string[])?.[0] === 'read:test', + 'Permission should match' + ) + console.log('✓ All assertions passed') +} + +// ============================================================================ +// Run all examples +// ============================================================================ + +async function main() { + try { + await exampleHS512() + // await exampleEdDSA() // Uncomment with real keys + await exampleDelegation() + await exampleDevEnvironment() + await exampleTesting() + + console.log('\n✓ All examples completed successfully!\n') + } catch (error) { + console.error('Error running examples:', error) + process.exit(1) + } +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main() +} diff --git a/package.json b/package.json index 2bbb936..0adcb35 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "typecheck:js": "cd packages/flarelette-jwt-ts && tsc --noEmit & cd ../..", "typecheck:py": "mypy packages/flarelette-jwt-py", "licenses:generate": "node scripts/generate-licenses.mjs", - "check": "npm run lint && npm run format:check && npm run typecheck && npm test", + "check": "npm run lint && npm run licenses:generate && npm run archlette && npm run format && npm run typecheck && npm run test:coverage", "pack:test": "npm run pack:test:ts && npm run pack:test:py", "pack:test:ts": "npm run build && cd packages/flarelette-jwt-ts && npm pack --pack-destination ./dist && cd ../.. && node scripts/inspect-pack-ts.js", "pack:test:py": "cd packages/flarelette-jwt-py && py prepare.py && python -m build && cd ../.. && node scripts/inspect-pack-py.js", diff --git a/packages/THIRD_PARTY_LICENSES.md b/packages/THIRD_PARTY_LICENSES.md index e831cb3..a4aef7e 100644 --- a/packages/THIRD_PARTY_LICENSES.md +++ b/packages/THIRD_PARTY_LICENSES.md @@ -14,10 +14,10 @@ The TypeScript package depends on the following NPM packages: ### TypeScript Package Dependencies Summary ``` -@flarelette/jwt-kit-env@1.8.0 +@flarelette/jwt-kit-env@1.8.1 │ C:\Users\chris\git\flarelette-jwt-kit │ -└─┬ @chrislyons-dev/flarelette-jwt@1.8.0 -> .\packages\flarelette-jwt-ts +└─┬ @chrislyons-dev/flarelette-jwt@1.9.0 -> .\packages\flarelette-jwt-ts │ Environment-driven JWT authentication for Cloudflare Workers with secret-name indirection └── jose@5.10.0 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-10-31 +**Last generated**: 2025-11-02 diff --git a/packages/flarelette-jwt-py/flarelette_jwt/__init__.py b/packages/flarelette-jwt-py/flarelette_jwt/__init__.py index 676c6b1..bafb2a8 100644 --- a/packages/flarelette-jwt-py/flarelette_jwt/__init__.py +++ b/packages/flarelette-jwt-py/flarelette_jwt/__init__.py @@ -17,6 +17,26 @@ mode, profile, ) +from .explicit import ( + AuthUser as AuthUserWithConfig, +) +from .explicit import ( + AuthzOptsWithConfig, + BaseJwtConfig, + EdDSASignConfig, + EdDSAVerifyConfig, + HS512Config, + SignConfig, + VerifyConfig, + check_auth_with_config, + create_delegated_token_with_config, + create_eddsa_sign_config, + create_eddsa_verify_config, + create_hs512_config, + create_token_with_config, + sign_with_config, + verify_with_config, +) from .high import AuthUser, check_auth, create_delegated_token, create_token, policy from .secret import generate_secret, is_valid_base64url_secret from .sign import sign @@ -36,7 +56,16 @@ "ActorClaim", "ParsedJwt", "AuthUser", - # Functions + # Explicit config types + "BaseJwtConfig", + "HS512Config", + "EdDSASignConfig", + "EdDSAVerifyConfig", + "SignConfig", + "VerifyConfig", + "AuthzOptsWithConfig", + "AuthUserWithConfig", + # Environment-based functions "common", "mode", "profile", @@ -51,4 +80,13 @@ "map_scopes_to_permissions", "parse", "verify", + # Explicit config functions + "sign_with_config", + "verify_with_config", + "create_token_with_config", + "create_delegated_token_with_config", + "check_auth_with_config", + "create_hs512_config", + "create_eddsa_sign_config", + "create_eddsa_verify_config", ] diff --git a/packages/flarelette-jwt-py/flarelette_jwt/explicit.py b/packages/flarelette-jwt-py/flarelette_jwt/explicit.py new file mode 100644 index 0000000..f093fe6 --- /dev/null +++ b/packages/flarelette-jwt-py/flarelette_jwt/explicit.py @@ -0,0 +1,625 @@ +""" +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. + +@module explicit +""" + +from __future__ import annotations + +import base64 +import json +import time +from typing import TYPE_CHECKING, Any, Literal, TypedDict + +if TYPE_CHECKING: + from collections.abc import Callable + + from .env import JwtPayload + + +class BaseJwtConfig(TypedDict, total=False): + """Base JWT configuration shared by HS512 and EdDSA modes. + + Attributes: + iss: Token issuer (iss claim) + aud: Token audience (aud claim) - can be string or list + ttl_seconds: Token lifetime in seconds (default: 900 = 15 minutes) + leeway: Clock skew tolerance in seconds for verification (default: 90) + """ + + iss: str + aud: str | list[str] + ttl_seconds: int + leeway: int + + +class HS512Config(BaseJwtConfig): + """HS512 (HMAC-SHA512) symmetric configuration. + + Uses a shared secret for both signing and verification. + + Attributes: + alg: Must be 'HS512' + secret: Shared secret key as bytes (minimum 32 bytes) + """ + + alg: Literal["HS512"] + secret: bytes + + +class EdDSASignConfig(BaseJwtConfig): + """EdDSA (Ed25519) asymmetric configuration for signing. + + Uses a private key to sign tokens. + + Attributes: + alg: Must be 'EdDSA' + private_jwk: Private JWK dictionary for signing + kid: Key ID to include in JWT header (optional) + """ + + alg: Literal["EdDSA"] + private_jwk: dict[str, Any] + kid: str | None + + +class EdDSAVerifyConfig(BaseJwtConfig): + """EdDSA (Ed25519) asymmetric configuration for verification. + + Uses a public key to verify tokens. + + Attributes: + alg: Must be 'EdDSA' + public_jwk: Public JWK dictionary for verification + """ + + alg: Literal["EdDSA"] + public_jwk: dict[str, Any] + + +# Union types for convenience +SignConfig = HS512Config | EdDSASignConfig +VerifyConfig = HS512Config | EdDSAVerifyConfig + + +def _b64url(b: bytes) -> str: + """Encode bytes to base64url without padding.""" + return base64.urlsafe_b64encode(b).decode("utf-8").rstrip("=") + + +def _b64url_decode(s: str) -> bytes: + """Decode base64url string (with or without padding).""" + return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) + + +async def sign_with_config( + payload: JwtPayload, + config: SignConfig, + *, + iss: str | None = None, + aud: str | list[str] | None = None, + ttl_seconds: int | None = None, +) -> str: + """Sign a JWT token with explicit configuration. + + Examples: + HS512 mode: + >>> config: HS512Config = { + ... 'alg': 'HS512', + ... 'secret': b'your-32-byte-secret-here...', + ... 'iss': 'https://gateway.example.com', + ... 'aud': 'api.example.com', + ... 'ttl_seconds': 900 + ... } + >>> token = await sign_with_config({'sub': 'user123'}, config) + + EdDSA mode: + >>> config: EdDSASignConfig = { + ... 'alg': 'EdDSA', + ... 'private_jwk': {'kty': 'OKP', 'crv': 'Ed25519', 'd': '...', 'x': '...'}, + ... 'kid': 'ed25519-2025-01', + ... 'iss': 'https://gateway.example.com', + ... 'aud': 'api.example.com' + ... } + >>> token = await sign_with_config({'sub': 'user123'}, config) + + Args: + payload: Claims to include in the token + config: Explicit JWT configuration + iss: Optional per-call override for issuer + aud: Optional per-call override for audience + ttl_seconds: Optional per-call override for TTL + + Returns: + Signed JWT token string + + Raises: + ValueError: If secret is too short (< 32 bytes) + RuntimeError: If EdDSA signing is attempted (not supported in Python Workers) + """ + iss_val = iss or config.get("iss", "") + aud_val = aud or config.get("aud", "") + ttl = ttl_seconds or config.get("ttl_seconds", 900) + + now = int(time.time()) + body = dict(payload) + body.setdefault("iss", iss_val) + body.setdefault("aud", aud_val) + body.setdefault("iat", now) + body.setdefault("exp", now + ttl) + + if config["alg"] == "HS512": + secret = config["secret"] + if len(secret) < 32: + raise ValueError(f"JWT secret too short: {len(secret)} bytes, need >= 32") + + # Lazy import - only available in Cloudflare Workers/Pyodide runtime + from js import crypto # noqa: PLC0415 + from pyodide.ffi import to_py # noqa: PLC0415 + + header = {"alg": "HS512", "typ": "JWT"} + h = _b64url(json.dumps(header, separators=(",", ":")).encode()) + p = _b64url(json.dumps(body, separators=(",", ":")).encode()) + signing_input = f"{h}.{p}".encode() + + key = await crypto.subtle.importKey( + "raw", + secret, + {"name": "HMAC", "hash": "SHA-512"}, + False, + ["sign"], + ) + sig = await crypto.subtle.sign({"name": "HMAC"}, key, signing_input) + + return f"{h}.{p}.{_b64url(bytes(to_py(sig)))}" + else: + # EdDSA mode + raise RuntimeError( + "EdDSA signing is not supported in Workers Python; produce tokens with the Node gateway" + ) + + +async def verify_with_config( + token: str, + config: VerifyConfig, + *, + iss: str | None = None, + aud: str | list[str] | None = None, + leeway: int | None = None, +) -> JwtPayload | None: + """Verify a JWT token with explicit configuration. + + Examples: + HS512 mode: + >>> config: HS512Config = { + ... 'alg': 'HS512', + ... 'secret': b'your-32-byte-secret-here...', + ... 'iss': 'https://gateway.example.com', + ... 'aud': 'api.example.com' + ... } + >>> payload = await verify_with_config(token, config) + + EdDSA mode: + >>> config: EdDSAVerifyConfig = { + ... 'alg': 'EdDSA', + ... 'public_jwk': {'kty': 'OKP', 'crv': 'Ed25519', 'x': '...'}, + ... 'iss': 'https://gateway.example.com', + ... 'aud': 'api.example.com' + ... } + >>> payload = await verify_with_config(token, config) + + Args: + token: JWT token string to verify + config: Explicit JWT configuration + iss: Optional per-call override for issuer + aud: Optional per-call override for audience + leeway: Optional per-call override for clock skew tolerance + + Returns: + Payload if valid, None if invalid + """ + iss_val = iss or config.get("iss", "") + aud_val = aud or config.get("aud", "") + leeway_val = leeway or config.get("leeway", 90) + + try: + h_b64, p_b64, s_b64 = token.split(".") + header = json.loads(_b64url_decode(h_b64)) + payload: JwtPayload = json.loads(_b64url_decode(p_b64)) + sig = _b64url_decode(s_b64) + except Exception: + return None + + # Lazy import - only available in Cloudflare Workers/Pyodide runtime + from js import crypto # noqa: PLC0415 + + if config["alg"] == "HS512": + if header.get("alg") != "HS512": + return None + + secret = config["secret"] + if len(secret) < 32: + return None + + key = await crypto.subtle.importKey( + "raw", + secret, + {"name": "HMAC", "hash": "SHA-512"}, + False, + ["verify"], + ) + ok = await crypto.subtle.verify( + {"name": "HMAC"}, key, sig, (h_b64 + "." + p_b64).encode() + ) + if not ok: + return None + else: + # EdDSA mode + if header.get("alg") != "EdDSA": + return None + + jwk = config["public_jwk"] + x_b64 = jwk.get("x") + if not x_b64: + return None + + x = _b64url_decode(x_b64) + key = await crypto.subtle.importKey( + "raw", x, {"name": "Ed25519"}, False, ["verify"] + ) + ok = await crypto.subtle.verify( + {"name": "Ed25519"}, key, sig, (h_b64 + "." + p_b64).encode() + ) + if not ok: + return None + + # Validate claims + now = int(time.time()) + if payload.get("iss") != iss_val: + return None + if payload.get("aud") != aud_val: + return None + if now > int(payload.get("exp", 0)) + leeway_val: + return None + nbf = int(payload.get("nbf", payload.get("iat", 0))) + if now + leeway_val < nbf: + return None + + return payload + + +async def create_token_with_config( + claims: JwtPayload, + config: SignConfig, + *, + iss: str | None = None, + aud: str | list[str] | None = None, + ttl_seconds: int | None = None, +) -> str: + """Create a signed JWT token with explicit configuration. + + Higher-level wrapper around sign_with_config for convenience. + + Args: + claims: Claims to include in the token + config: Explicit JWT configuration + iss: Optional per-call override for issuer + aud: Optional per-call override for audience + ttl_seconds: Optional per-call override for TTL + + Returns: + Signed JWT token string + """ + return await sign_with_config( + claims, config, iss=iss, aud=aud, ttl_seconds=ttl_seconds + ) + + +async def create_delegated_token_with_config( + original_payload: JwtPayload, + actor_service: str, + config: SignConfig, + *, + iss: str | None = None, + aud: str | list[str] | None = None, + ttl_seconds: int | None = None, +) -> str: + """Create a delegated JWT token with explicit configuration. + + Implements RFC 8693 actor claim pattern for service-to-service delegation. + + Example: + >>> config: HS512Config = { + ... 'alg': 'HS512', + ... 'secret': b'my-secret...', + ... 'iss': 'https://gateway.example.com', + ... 'aud': 'internal-api' + ... } + >>> # Gateway receives Auth0 token and creates delegated token + >>> auth0_payload = await verify_auth0_token(external_token) + >>> internal_token = await create_delegated_token_with_config( + ... auth0_payload, + ... 'gateway-service', + ... config + ... ) + + Args: + original_payload: The verified JWT payload from external auth + actor_service: Identifier of the service creating this delegated token + config: Explicit JWT configuration + iss: Optional per-call override for issuer + aud: Optional per-call override for audience + ttl_seconds: Optional per-call override for TTL + + Returns: + Signed JWT token string with delegation claim + """ + # Preserve original user context and permissions + delegated_claims: dict[str, Any] = { + "sub": original_payload.get("sub"), + "permissions": original_payload.get("permissions", []), + "roles": original_payload.get("roles", []), + } + + # Add actor claim + existing_act = original_payload.get("act") + if existing_act: + delegated_claims["act"] = {"sub": actor_service, "act": existing_act} + else: + delegated_claims["act"] = {"sub": actor_service} + + # Preserve additional context fields + if "email" in original_payload: + delegated_claims["email"] = original_payload["email"] + if "name" in original_payload: + delegated_claims["name"] = original_payload["name"] + if "groups" in original_payload: + delegated_claims["groups"] = original_payload["groups"] + if "tid" in original_payload: + delegated_claims["tid"] = original_payload["tid"] + if "org_id" in original_payload: + delegated_claims["org_id"] = original_payload["org_id"] + if "department" in original_payload: + delegated_claims["department"] = original_payload["department"] + + return await sign_with_config( + delegated_claims, # type: ignore[arg-type] + config, + iss=iss, + aud=aud, + ttl_seconds=ttl_seconds, + ) + + +class AuthzOptsWithConfig(TypedDict, total=False): + """Authorization options for check_auth_with_config. + + Attributes: + require_all_permissions: All permissions must be present + require_any_permission: At least one permission must be present + require_roles_all: All roles must be present + require_roles_any: At least one role must be present + predicates: Custom predicate functions that must all return True + """ + + require_all_permissions: list[str] + require_any_permission: list[str] + require_roles_all: list[str] + require_roles_any: list[str] + predicates: list[Callable[[JwtPayload], bool]] + + +class AuthUser(TypedDict, total=False): + """Authenticated user information. + + Returned when a token passes both verification and authorization. + + Attributes: + sub: Subject identifier + permissions: List of permission strings + roles: List of role strings + jti: JWT ID + payload: Complete JWT payload + """ + + sub: str | None + permissions: list[str] + roles: list[str] + jti: str | None + payload: JwtPayload + + +async def check_auth_with_config( + token: str, + config: VerifyConfig, + authz_opts: AuthzOptsWithConfig | None = None, + *, + iss: str | None = None, + aud: str | list[str] | None = None, + leeway: int | None = None, +) -> AuthUser | None: + """Verify and authorize a JWT token with explicit configuration. + + Example: + >>> config: HS512Config = { + ... 'alg': 'HS512', + ... 'secret': b'my-secret...', + ... 'iss': 'https://gateway.example.com', + ... 'aud': 'api.example.com' + ... } + >>> user = await check_auth_with_config(token, config, { + ... 'require_all_permissions': ['read:data'], + ... 'require_any_permission': ['admin', 'editor'] + ... }) + >>> if user: + ... print('Authorized user:', user['sub']) + + Args: + token: JWT token string to verify + config: Explicit JWT configuration + authz_opts: Authorization policy requirements + iss: Optional per-call override for issuer + aud: Optional per-call override for audience + leeway: Optional per-call override for clock skew tolerance + + Returns: + AuthUser if valid and authorized, None otherwise + """ + payload = await verify_with_config(token, config, iss=iss, aud=aud, leeway=leeway) + if not payload: + return None + + opts = authz_opts or {} + perms = payload.get("permissions", []) + roles = payload.get("roles", []) + + # Check all required permissions + if opts.get("require_all_permissions") and not all( + p in perms for p in opts["require_all_permissions"] + ): + return None + + # Check any required permission + if opts.get("require_any_permission") and not any( + p in perms for p in opts["require_any_permission"] + ): + return None + + # Check all required roles + if opts.get("require_roles_all") and not all( + r in roles for r in opts["require_roles_all"] + ): + return None + + # Check any required role + if opts.get("require_roles_any") and not any( + r in roles for r in opts["require_roles_any"] + ): + return None + + # Check custom predicates + for pred in opts.get("predicates", []): + if not pred(payload): + return None + + return { + "sub": payload.get("sub"), + "permissions": perms, + "roles": roles, + "jti": payload.get("jti"), + "payload": payload, + } + + +def create_hs512_config( + secret: str | bytes, + *, + iss: str, + aud: str | list[str], + ttl_seconds: int = 900, + leeway: int = 90, +) -> HS512Config: + """Helper function to create HS512 config from base64url-encoded secret. + + Args: + secret: Base64url-encoded secret string or raw bytes (minimum 32 bytes) + iss: Token issuer + aud: Token audience (string or list) + ttl_seconds: Token lifetime in seconds (default: 900 = 15 minutes) + leeway: Clock skew tolerance in seconds (default: 90) + + Returns: + HS512Config + + Raises: + ValueError: If secret is too short (< 32 bytes) + """ + if isinstance(secret, str): + # Decode base64url + secret_bytes = _b64url_decode(secret.replace("-", "+").replace("_", "/")) + else: + secret_bytes = secret + + if len(secret_bytes) < 32: + raise ValueError(f"JWT secret too short: {len(secret_bytes)} bytes, need >= 32") + + return { + "alg": "HS512", + "secret": secret_bytes, + "iss": iss, + "aud": aud, + "ttl_seconds": ttl_seconds, + "leeway": leeway, + } + + +def create_eddsa_sign_config( + private_jwk: dict[str, Any] | str, + *, + iss: str, + aud: str | list[str], + kid: str | None = None, + ttl_seconds: int = 900, + leeway: int = 90, +) -> EdDSASignConfig: + """Helper function to create EdDSA sign config from JWK. + + Args: + private_jwk: Private JWK dictionary or JSON string + iss: Token issuer + aud: Token audience (string or list) + kid: Key ID (optional) + ttl_seconds: Token lifetime in seconds (default: 900 = 15 minutes) + leeway: Clock skew tolerance in seconds (default: 90) + + Returns: + EdDSASignConfig + """ + jwk = json.loads(private_jwk) if isinstance(private_jwk, str) else private_jwk + + return { + "alg": "EdDSA", + "private_jwk": jwk, + "kid": kid, + "iss": iss, + "aud": aud, + "ttl_seconds": ttl_seconds, + "leeway": leeway, + } + + +def create_eddsa_verify_config( + public_jwk: dict[str, Any] | str, + *, + iss: str, + aud: str | list[str], + ttl_seconds: int = 900, + leeway: int = 90, +) -> EdDSAVerifyConfig: + """Helper function to create EdDSA verify config from JWK. + + Args: + public_jwk: Public JWK dictionary or JSON string + iss: Token issuer + aud: Token audience (string or list) + ttl_seconds: Token lifetime in seconds (default: 900 = 15 minutes) + leeway: Clock skew tolerance in seconds (default: 90) + + Returns: + EdDSAVerifyConfig + """ + jwk = json.loads(public_jwk) if isinstance(public_jwk, str) else public_jwk + + return { + "alg": "EdDSA", + "public_jwk": jwk, + "iss": iss, + "aud": aud, + "ttl_seconds": ttl_seconds, + "leeway": leeway, + } diff --git a/packages/flarelette-jwt-ts/src/explicit.ts b/packages/flarelette-jwt-ts/src/explicit.ts new file mode 100644 index 0000000..45d5ba1 --- /dev/null +++ b/packages/flarelette-jwt-ts/src/explicit.ts @@ -0,0 +1,447 @@ +/** + * 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. + * + * @module explicit + */ + +import { SignJWT, jwtVerify, importJWK, type JWTVerifyResult, type JWK } from 'jose' +import type { JwtPayload } from './types.js' + +/** + * Base JWT configuration shared by HS512 and EdDSA modes + */ +export interface BaseJwtConfig { + /** Token issuer (iss claim) */ + iss: string + /** Token audience (aud claim) - can be string or array */ + aud: string | string[] + /** Token lifetime in seconds (default: 900 = 15 minutes) */ + ttlSeconds?: number + /** Clock skew tolerance in seconds for verification (default: 90) */ + leeway?: number +} + +/** + * HS512 (HMAC-SHA512) symmetric configuration + * Uses a shared secret for both signing and verification + */ +export interface HS512Config extends BaseJwtConfig { + alg: 'HS512' + /** Shared secret key as Uint8Array (minimum 32 bytes) */ + secret: Uint8Array +} + +/** + * EdDSA (Ed25519) asymmetric configuration for signing + * Uses a private key to sign tokens + */ +export interface EdDSASignConfig extends BaseJwtConfig { + alg: 'EdDSA' + /** Private JWK for signing */ + privateJwk: JWK + /** Key ID to include in JWT header */ + kid?: string +} + +/** + * EdDSA (Ed25519) asymmetric configuration for verification + * Uses a public key to verify tokens + */ +export interface EdDSAVerifyConfig extends BaseJwtConfig { + alg: 'EdDSA' + /** Public JWK for verification */ + publicJwk: JWK +} + +/** + * Union type for signing configuration + */ +export type SignConfig = HS512Config | EdDSASignConfig + +/** + * Union type for verification configuration + */ +export type VerifyConfig = HS512Config | EdDSAVerifyConfig + +/** + * Sign a JWT token with explicit configuration + * + * @example + * ```typescript + * // HS512 mode + * const config: HS512Config = { + * alg: 'HS512', + * secret: new Uint8Array(32), // Your secret + * iss: 'https://gateway.example.com', + * aud: 'api.example.com', + * ttlSeconds: 900 + * } + * const token = await signWithConfig({ sub: 'user123' }, config) + * + * // EdDSA mode + * const config: EdDSASignConfig = { + * alg: 'EdDSA', + * privateJwk: { kty: 'OKP', crv: 'Ed25519', d: '...', x: '...' }, + * kid: 'ed25519-2025-01', + * iss: 'https://gateway.example.com', + * aud: 'api.example.com' + * } + * const token = await signWithConfig({ sub: 'user123' }, config) + * ``` + * + * @param payload - Claims to include in the token + * @param config - Explicit JWT configuration + * @param overrides - Optional per-call overrides for iss, aud, ttlSeconds + * @returns Signed JWT token string + */ +export async function signWithConfig( + payload: JwtPayload, + config: SignConfig, + overrides?: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number }> +): Promise { + const iss = overrides?.iss ?? config.iss + const aud = overrides?.aud ?? config.aud + const ttlSeconds = overrides?.ttlSeconds ?? config.ttlSeconds ?? 900 + + const now = Math.floor(Date.now() / 1000) + const jwt = new SignJWT(payload) + .setIssuer(iss) + .setAudience(aud) + .setIssuedAt(now) + .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`) + } + return jwt.setProtectedHeader({ alg: 'HS512', typ: 'JWT' }).sign(config.secret) + } else { + const key = await importJWK(config.privateJwk, 'EdDSA') + return jwt + .setProtectedHeader({ alg: 'EdDSA', typ: 'JWT', kid: config.kid }) + .sign(key) + } +} + +/** + * Verify a JWT token with explicit configuration + * + * @example + * ```typescript + * // HS512 mode + * const config: HS512Config = { + * alg: 'HS512', + * secret: new Uint8Array(32), // Same secret used for signing + * iss: 'https://gateway.example.com', + * aud: 'api.example.com' + * } + * const payload = await verifyWithConfig(token, config) + * + * // EdDSA mode + * const config: EdDSAVerifyConfig = { + * alg: 'EdDSA', + * publicJwk: { kty: 'OKP', crv: 'Ed25519', x: '...' }, + * iss: 'https://gateway.example.com', + * aud: 'api.example.com' + * } + * const payload = await verifyWithConfig(token, config) + * ``` + * + * @param token - JWT token string to verify + * @param config - Explicit JWT configuration + * @param overrides - Optional per-call overrides for iss, aud, leeway + * @returns Payload if valid, null if invalid + */ +export async function verifyWithConfig( + token: string, + config: VerifyConfig, + overrides?: Partial<{ iss: string; aud: string | string[]; leeway: number }> +): Promise { + try { + const iss = overrides?.iss ?? config.iss + const aud = overrides?.aud ?? config.aud + const leeway = overrides?.leeway ?? config.leeway ?? 90 + + let result: JWTVerifyResult + + if (config.alg === 'HS512') { + if (config.secret.length < 32) { + throw new Error( + `JWT secret too short: ${config.secret.length} bytes, need >= 32` + ) + } + result = await jwtVerify(token, config.secret, { + issuer: iss, + audience: aud, + clockTolerance: leeway, + }) + } else { + const key = await importJWK(config.publicJwk, 'EdDSA') + result = await jwtVerify(token, key, { + issuer: iss, + audience: aud, + clockTolerance: leeway, + }) + } + + return result.payload as JwtPayload + } catch (e) { + console.error('JWT verification failed:', e) + return null + } +} + +/** + * Create a signed JWT token with explicit configuration + * + * Higher-level wrapper around signWithConfig for convenience. + * + * @param claims - Claims to include in the token + * @param config - Explicit JWT configuration + * @param overrides - Optional per-call overrides + * @returns Signed JWT token string + */ +export async function createTokenWithConfig( + claims: JwtPayload, + config: SignConfig, + overrides?: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number }> +): Promise { + return signWithConfig(claims, config, overrides) +} + +/** + * Create a delegated JWT token with explicit configuration + * + * Implements RFC 8693 actor claim pattern for service-to-service delegation. + * + * @example + * ```typescript + * const config: HS512Config = { + * alg: 'HS512', + * secret: mySecret, + * iss: 'https://gateway.example.com', + * aud: 'internal-api' + * } + * + * // Gateway receives Auth0 token and creates delegated token + * const auth0Payload = await verifyAuth0Token(externalToken) + * const internalToken = await createDelegatedTokenWithConfig( + * auth0Payload, + * 'gateway-service', + * config + * ) + * ``` + * + * @param originalPayload - The verified JWT payload from external auth + * @param actorService - Identifier of the service creating this delegated token + * @param config - Explicit JWT configuration + * @param overrides - Optional per-call overrides + * @returns Signed JWT token string with delegation claim + */ +export async function createDelegatedTokenWithConfig( + originalPayload: JwtPayload, + actorService: string, + config: SignConfig, + overrides?: Partial<{ iss: string; aud: string | string[]; ttlSeconds: number }> +): Promise { + // Preserve original user context and permissions + const delegatedClaims: JwtPayload = { + sub: originalPayload.sub, + permissions: originalPayload.permissions || [], + roles: originalPayload.roles || [], + } + + // Add actor claim + const existingAct = originalPayload.act + if (existingAct) { + delegatedClaims.act = { + sub: actorService, + act: existingAct, + } + } else { + delegatedClaims.act = { sub: actorService } + } + + // Preserve additional context fields + if (originalPayload.email) delegatedClaims.email = originalPayload.email + if (originalPayload.name) delegatedClaims.name = originalPayload.name + if (originalPayload.groups) delegatedClaims.groups = originalPayload.groups + if (originalPayload.tid) delegatedClaims.tid = originalPayload.tid + if (originalPayload.org_id) delegatedClaims.org_id = originalPayload.org_id + if (originalPayload.department) + delegatedClaims.department = originalPayload.department + + return signWithConfig(delegatedClaims, config, overrides) +} + +/** + * Authorization options for checkAuthWithConfig + */ +export type AuthzOptsWithConfig = { + require_all_permissions?: string[] + require_any_permission?: string[] + require_roles_all?: string[] + require_roles_any?: string[] + predicates?: Array<(payload: JwtPayload) => boolean> +} + +/** + * Authenticated user information + */ +export type AuthUser = { + sub: string | undefined + permissions: string[] + roles: string[] + jti: string | undefined + payload: JwtPayload +} + +/** + * Verify and authorize a JWT token with explicit configuration + * + * @example + * ```typescript + * const config: HS512Config = { + * alg: 'HS512', + * secret: mySecret, + * iss: 'https://gateway.example.com', + * aud: 'api.example.com' + * } + * + * const user = await checkAuthWithConfig(token, config, { + * require_all_permissions: ['read:data'], + * require_any_permission: ['admin', 'editor'] + * }) + * + * if (user) { + * console.log('Authorized user:', user.sub) + * } + * ``` + * + * @param token - JWT token string to verify + * @param config - Explicit JWT configuration + * @param authzOpts - Authorization policy requirements + * @param verifyOverrides - Optional per-call verification overrides + * @returns AuthUser if valid and authorized, null otherwise + */ +export async function checkAuthWithConfig( + token: string, + config: VerifyConfig, + authzOpts?: AuthzOptsWithConfig, + verifyOverrides?: Partial<{ iss: string; aud: string | string[]; leeway: number }> +): Promise { + const payload = await verifyWithConfig(token, config, verifyOverrides) + if (!payload) return null + + const opts = authzOpts || {} + const perms = (payload.permissions as string[]) || [] + const roles = (payload.roles as string[]) || [] + + // Check all required permissions + if ((opts.require_all_permissions || []).some(p => !perms.includes(p))) return null + + // Check any required permission + if ( + (opts.require_any_permission || []).length && + !(opts.require_any_permission || []).some(p => perms.includes(p)) + ) + return null + + // Check all required roles + if ((opts.require_roles_all || []).some(r => !roles.includes(r))) return null + + // Check any required role + if ( + (opts.require_roles_any || []).length && + !(opts.require_roles_any || []).some(r => roles.includes(r)) + ) + return null + + // Check custom predicates + for (const pred of opts.predicates || []) { + if (!pred(payload)) return null + } + + return { + sub: payload.sub as string | undefined, + permissions: perms, + roles, + jti: payload.jti as string | undefined, + payload, + } +} + +/** + * Helper function to create HS512 config from base64url-encoded secret + * + * @param secret - Base64url-encoded secret string + * @param baseConfig - Base JWT configuration + * @returns HS512 configuration + */ +export function createHS512Config( + secret: string, + baseConfig: Omit & + Partial> +): HS512Config { + // Decode base64url + 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`) + } + + return { + alg: 'HS512', + secret: new Uint8Array(buf), + ...baseConfig, + } +} + +/** + * Helper function to create EdDSA sign config from JWK + * + * @param privateJwk - Private JWK object or JSON string + * @param baseConfig - Base JWT configuration + * @param kid - Optional key ID + * @returns EdDSA sign configuration + */ +export function createEdDSASignConfig( + privateJwk: JWK | string, + baseConfig: Omit & + Partial>, + kid?: string +): EdDSASignConfig { + const jwk = typeof privateJwk === 'string' ? JSON.parse(privateJwk) : privateJwk + return { + alg: 'EdDSA', + privateJwk: jwk, + kid, + ...baseConfig, + } +} + +/** + * Helper function to create EdDSA verify config from JWK + * + * @param publicJwk - Public JWK object or JSON string + * @param baseConfig - Base JWT configuration + * @returns EdDSA verify configuration + */ +export function createEdDSAVerifyConfig( + publicJwk: JWK | string, + baseConfig: Omit & + Partial> +): EdDSAVerifyConfig { + const jwk = typeof publicJwk === 'string' ? JSON.parse(publicJwk) : publicJwk + return { + alg: 'EdDSA', + publicJwk: jwk, + ...baseConfig, + } +} diff --git a/packages/flarelette-jwt-ts/src/index.ts b/packages/flarelette-jwt-ts/src/index.ts index b4734ef..1656509 100644 --- a/packages/flarelette-jwt-ts/src/index.ts +++ b/packages/flarelette-jwt-ts/src/index.ts @@ -50,3 +50,25 @@ export type { // Adapters export * as adapters from './adapters/hono.js' + +// Explicit configuration API (no environment dependencies) +export { + signWithConfig, + verifyWithConfig, + createTokenWithConfig, + createDelegatedTokenWithConfig, + checkAuthWithConfig, + createHS512Config, + createEdDSASignConfig, + createEdDSAVerifyConfig, +} from './explicit.js' + +export type { + BaseJwtConfig, + HS512Config, + EdDSASignConfig, + EdDSAVerifyConfig, + SignConfig, + VerifyConfig, + AuthzOptsWithConfig, +} from './explicit.js' diff --git a/packages/flarelette-jwt-ts/tests/explicit.test.ts b/packages/flarelette-jwt-ts/tests/explicit.test.ts new file mode 100644 index 0000000..2c71a2f --- /dev/null +++ b/packages/flarelette-jwt-ts/tests/explicit.test.ts @@ -0,0 +1,460 @@ +/** + * Tests for explicit configuration API + * + * These tests verify the new explicit config API works without + * any environment variables or global state. + */ + +import { describe, it, expect, beforeEach } from 'vitest' +import { + signWithConfig, + verifyWithConfig, + createTokenWithConfig, + createDelegatedTokenWithConfig, + checkAuthWithConfig, + createHS512Config, + type HS512Config, +} from '../src/explicit.js' + +describe('Explicit Configuration API', () => { + let testConfig: HS512Config + + beforeEach(() => { + // Create a fresh config for each test - no environment pollution! + const secret = new Uint8Array(32).fill(42) // 32 bytes, all set to 42 + testConfig = { + alg: 'HS512', + secret, + iss: 'test-issuer', + aud: 'test-audience', + ttlSeconds: 900, + leeway: 90, + } + }) + + describe('signWithConfig', () => { + it('should sign a token with explicit config', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig) + + expect(token).toBeDefined() + expect(typeof token).toBe('string') + expect(token.split('.')).toHaveLength(3) // JWT has 3 parts + }) + + it('should include custom claims', async () => { + const token = await signWithConfig( + { + sub: 'user123', + email: 'test@example.com', + permissions: ['read:data', 'write:data'], + }, + testConfig + ) + + const payload = await verifyWithConfig(token, testConfig) + expect(payload?.sub).toBe('user123') + expect(payload?.email).toBe('test@example.com') + expect(payload?.permissions).toEqual(['read:data', 'write:data']) + }) + + it('should support per-call overrides', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig, { + iss: 'override-issuer', + aud: 'override-audience', + ttlSeconds: 300, + }) + + const payload = await verifyWithConfig(token, { + ...testConfig, + iss: 'override-issuer', + aud: 'override-audience', + }) + + expect(payload?.sub).toBe('user123') + }) + + it('should reject secrets shorter than 32 bytes', async () => { + const shortConfig = { + ...testConfig, + secret: new Uint8Array(16), // Too short! + } + + await expect(signWithConfig({ sub: 'test' }, shortConfig)).rejects.toThrow( + 'JWT secret too short' + ) + }) + }) + + describe('verifyWithConfig', () => { + it('should verify a valid token', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig) + const payload = await verifyWithConfig(token, testConfig) + + expect(payload).toBeDefined() + expect(payload?.sub).toBe('user123') + expect(payload?.iss).toBe('test-issuer') + expect(payload?.aud).toBe('test-audience') + }) + + it('should reject tokens with wrong issuer', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig) + + const wrongConfig = { ...testConfig, iss: 'wrong-issuer' } + const payload = await verifyWithConfig(token, wrongConfig) + + expect(payload).toBeNull() + }) + + it('should reject tokens with wrong audience', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig) + + const wrongConfig = { ...testConfig, aud: 'wrong-audience' } + const payload = await verifyWithConfig(token, wrongConfig) + + expect(payload).toBeNull() + }) + + it('should reject tokens with wrong secret', async () => { + const token = await signWithConfig({ sub: 'user123' }, testConfig) + + const wrongConfig = { + ...testConfig, + secret: new Uint8Array(32).fill(99), // Different secret + } + const payload = await verifyWithConfig(token, wrongConfig) + + expect(payload).toBeNull() + }) + + it('should reject expired tokens', async () => { + // Create token that expires way in the past (beyond leeway) + const token = await signWithConfig( + { sub: 'user123' }, + { ...testConfig, ttlSeconds: -200 } // Expired 200 seconds ago, beyond 90s leeway + ) + + const payload = await verifyWithConfig(token, testConfig) + expect(payload).toBeNull() + }) + + it('should handle clock skew with leeway', async () => { + // Token issued "in the future" but within leeway + const token = await signWithConfig({ sub: 'user123' }, testConfig) + + // Should still verify due to leeway + const payload = await verifyWithConfig(token, { + ...testConfig, + leeway: 120, // 2 minutes leeway + }) + + expect(payload).toBeDefined() + }) + }) + + describe('createTokenWithConfig', () => { + it('should create a token (convenience wrapper)', async () => { + const token = await createTokenWithConfig( + { sub: 'user123', permissions: ['admin'] }, + testConfig + ) + + const payload = await verifyWithConfig(token, testConfig) + expect(payload?.sub).toBe('user123') + expect(payload?.permissions).toEqual(['admin']) + }) + }) + + describe('createDelegatedTokenWithConfig', () => { + it('should create delegated token with actor claim', async () => { + const originalPayload = { + sub: 'auth0|user123', + email: 'user@example.com', + permissions: ['read:data', 'write:data'], + } + + const token = await createDelegatedTokenWithConfig( + originalPayload, + 'gateway-service', + testConfig + ) + + const payload = await verifyWithConfig(token, testConfig) + + expect(payload?.sub).toBe('auth0|user123') // Original user + expect(payload?.act).toEqual({ sub: 'gateway-service' }) // Actor + expect(payload?.permissions).toEqual(['read:data', 'write:data']) // Preserved + expect(payload?.email).toBe('user@example.com') // Preserved + }) + + it('should handle delegation chains', async () => { + const originalPayload = { + sub: 'user123', + permissions: ['read:data'], + act: { sub: 'first-service' }, + } + + const token = await createDelegatedTokenWithConfig( + originalPayload, + 'second-service', + testConfig + ) + + const payload = await verifyWithConfig(token, testConfig) + + expect(payload?.act).toEqual({ + sub: 'second-service', + act: { sub: 'first-service' }, + }) + }) + + it('should preserve optional fields', async () => { + const originalPayload = { + sub: 'user123', + permissions: ['read:data'], + roles: ['admin'], + groups: ['engineering'], + tid: 'tenant-1', + org_id: 'org-1', + department: 'IT', + } + + const token = await createDelegatedTokenWithConfig( + originalPayload, + 'gateway', + testConfig + ) + + const payload = await verifyWithConfig(token, testConfig) + + expect(payload?.roles).toEqual(['admin']) + expect(payload?.groups).toEqual(['engineering']) + expect(payload?.tid).toBe('tenant-1') + expect(payload?.org_id).toBe('org-1') + expect(payload?.department).toBe('IT') + }) + }) + + describe('checkAuthWithConfig', () => { + it('should authorize valid token', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + permissions: ['read:data', 'write:data'], + roles: ['user'], + }, + testConfig + ) + + const user = await checkAuthWithConfig(token, testConfig) + + expect(user).toBeDefined() + expect(user?.sub).toBe('user123') + expect(user?.permissions).toEqual(['read:data', 'write:data']) + expect(user?.roles).toEqual(['user']) + }) + + it('should enforce require_all_permissions', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + permissions: ['read:data'], // Missing write:data + }, + testConfig + ) + + const user = await checkAuthWithConfig(token, testConfig, { + require_all_permissions: ['read:data', 'write:data'], + }) + + expect(user).toBeNull() + }) + + it('should enforce require_any_permission', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + permissions: ['read:data'], + }, + testConfig + ) + + // Should pass - has one of the required permissions + const user1 = await checkAuthWithConfig(token, testConfig, { + require_any_permission: ['read:data', 'admin'], + }) + expect(user1).toBeDefined() + + // Should fail - has none of the required permissions + const user2 = await checkAuthWithConfig(token, testConfig, { + require_any_permission: ['write:data', 'admin'], + }) + expect(user2).toBeNull() + }) + + it('should enforce require_roles_all', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + roles: ['user', 'editor'], + }, + testConfig + ) + + const user = await checkAuthWithConfig(token, testConfig, { + require_roles_all: ['user', 'admin'], // Missing admin + }) + + expect(user).toBeNull() + }) + + it('should enforce require_roles_any', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + roles: ['user'], + }, + testConfig + ) + + const user = await checkAuthWithConfig(token, testConfig, { + require_roles_any: ['admin', 'editor'], // Has neither + }) + + expect(user).toBeNull() + }) + + it('should enforce custom predicates', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + email: 'user@example.com', + }, + testConfig + ) + + // Should pass + const user1 = await checkAuthWithConfig(token, testConfig, { + predicates: [ + payload => payload.email?.toString().endsWith('@example.com') || false, + ], + }) + expect(user1).toBeDefined() + + // Should fail + const user2 = await checkAuthWithConfig(token, testConfig, { + predicates: [ + payload => payload.email?.toString().endsWith('@other.com') || false, + ], + }) + expect(user2).toBeNull() + }) + + it('should combine multiple authorization rules', async () => { + const token = await createTokenWithConfig( + { + sub: 'user123', + email: 'admin@example.com', + permissions: ['read:data', 'write:data', 'admin'], + roles: ['admin', 'user'], + }, + testConfig + ) + + const user = await checkAuthWithConfig(token, testConfig, { + require_all_permissions: ['read:data', 'write:data'], + require_any_permission: ['admin', 'super-admin'], + require_roles_all: ['user'], + require_roles_any: ['admin', 'editor'], + predicates: [payload => payload.email?.toString().includes('admin') || false], + }) + + expect(user).toBeDefined() + expect(user?.sub).toBe('user123') + }) + }) + + 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') + + const config = createHS512Config(secret, { + iss: 'test-issuer', + aud: 'test-audience', + }) + + expect(config.alg).toBe('HS512') + expect(config.secret).toBeInstanceOf(Uint8Array) + expect(config.secret.length).toBe(32) + expect(config.iss).toBe('test-issuer') + expect(config.aud).toBe('test-audience') + }) + + it('should reject short secrets', () => { + const shortSecret = Buffer.alloc(16).toString('base64url') // Only 16 bytes + + expect(() => { + createHS512Config(shortSecret, { + iss: 'test', + aud: 'test', + }) + }).toThrow('JWT secret too short') + }) + }) + + describe('Isolation: No environment pollution', () => { + it('should work without any environment variables', async () => { + // Explicitly verify no JWT_ environment variables are used + const envBackup = process.env + process.env = {} // Clear all env vars + + try { + const config = { + alg: 'HS512' as const, + secret: new Uint8Array(32).fill(1), + iss: 'isolated-test', + aud: 'isolated-audience', + } + + const token = await signWithConfig({ sub: 'isolated-user' }, config) + const payload = await verifyWithConfig(token, config) + + expect(payload?.sub).toBe('isolated-user') + expect(payload?.iss).toBe('isolated-test') + } finally { + process.env = envBackup + } + }) + + it('should allow multiple configs in same process', async () => { + const config1 = { + alg: 'HS512' as const, + secret: new Uint8Array(32).fill(1), + iss: 'issuer-1', + aud: 'audience-1', + } + + const config2 = { + alg: 'HS512' as const, + secret: new Uint8Array(32).fill(2), + iss: 'issuer-2', + aud: 'audience-2', + } + + // Create tokens with different configs + const token1 = await signWithConfig({ sub: 'user1' }, config1) + const token2 = await signWithConfig({ sub: 'user2' }, config2) + + // Each token only verifies with its own config + const payload1 = await verifyWithConfig(token1, config1) + const payload2 = await verifyWithConfig(token2, config2) + const crossCheck1 = await verifyWithConfig(token1, config2) + const crossCheck2 = await verifyWithConfig(token2, config1) + + expect(payload1?.sub).toBe('user1') + expect(payload2?.sub).toBe('user2') + expect(crossCheck1).toBeNull() // Wrong config + expect(crossCheck2).toBeNull() // Wrong config + }) + }) +})