first commit
This commit is contained in:
65
docs/identity/auth/jwt-tenant/README.md
Normal file
65
docs/identity/auth/jwt-tenant/README.md
Normal file
File diff suppressed because one or more lines are too long
65
docs/identity/auth/jwt-tenant/src/errors/jwt-tenant-error.md
Normal file
65
docs/identity/auth/jwt-tenant/src/errors/jwt-tenant-error.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# jwt-tenant-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the structured error hierarchy for `@suaveplan/jwt-tenant`. All thrown errors extend `SuaveplanError` with a stable code registry under the `JWT.*` namespace. Using `defineCodes` ensures code strings are deduplicated at module load time and are inspectable programmatically without string matching.
|
||||
|
||||
## Features
|
||||
|
||||
- `JWT_TENANT_CODES` registry with 9 distinct error codes
|
||||
- `JwtTenantError` class extending `SuaveplanError<JwtTenantErrorCode>`
|
||||
- Cause-chain forwarding: non-Error causes are swallowed, Error causes preserved
|
||||
- Machine-inspectable error codes for upstream routing and logging
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { JwtTenantError, JWT_TENANT_CODES } from "@suaveplan/jwt-tenant";
|
||||
|
||||
throw new JwtTenantError(
|
||||
JWT_TENANT_CODES.EXPIRED,
|
||||
"Token has expired"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { JwtTenantError, JWT_TENANT_CODES } from "@suaveplan/jwt-tenant";
|
||||
|
||||
try {
|
||||
await verifyToken(token, key);
|
||||
} catch (raw) {
|
||||
if (raw instanceof Error) {
|
||||
throw new JwtTenantError(
|
||||
JWT_TENANT_CODES.INVALID_SIGNATURE,
|
||||
"Signature verification failed",
|
||||
{ cause: raw }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `JWT_TENANT_CODES`
|
||||
|
||||
Object mapping each error name to its fully qualified code string (e.g., `JWT.EXPIRED`). Produced by `defineCodes("JWT", [...])`.
|
||||
|
||||
### `JwtTenantError`
|
||||
|
||||
- **constructor(code, message, options?)** — Creates a structured error.
|
||||
- `code` — `JwtTenantErrorCode`
|
||||
- `message` — human-readable description
|
||||
- `options.cause` — optional underlying `Error` for cause-chain
|
||||
|
||||
### `JwtTenantErrorCode`
|
||||
|
||||
Union type of all code keys: `"JWT_INVALID_SIGNATURE" | "JWT_EXPIRED" | ... | "JWT_SERIALIZATION_FAILED"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`JwtTenantError` wraps the `SuaveplanError` constructor pattern established across Suaveplan. Non-Error causes are dropped rather than coerced because `SuaveplanError` only accepts `Error` in its cause chain — this prevents accidental serialisation of arbitrary objects.
|
||||
@@ -0,0 +1,74 @@
|
||||
# in-memory-key-store
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides an in-process, non-persistent implementation of `TenantKeyStore`. Used as the default key store in development, testing, and single-node deployments where distributed key sharing is not required. The injectable `Clock` makes expiry checks deterministic in unit tests without real time manipulation.
|
||||
|
||||
## Features
|
||||
|
||||
- `InMemoryKeyStore` implementing the full `TenantKeyStore` interface
|
||||
- `Map<keyId, KeyPair>` backing store with O(1) lookup
|
||||
- Expiry-aware `getActiveKeys()` — filters out keys where `clock() >= expiresAt`
|
||||
- `pruneExpired()` housekeeping method for long-running processes
|
||||
- `createInMemoryKeyStore(clock?)` factory function
|
||||
- Optional `tenantId` parameter in `getActiveKeys` (pass-through; use one store per tenant)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { InMemoryKeyStore } from "@suaveplan/jwt-tenant/jwks/in-memory-key-store";
|
||||
import { generateKeyPair } from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
|
||||
const store = new InMemoryKeyStore();
|
||||
const kp = await generateKeyPair("EdDSA");
|
||||
await store.addKey(kp);
|
||||
|
||||
const active = await store.getActiveKeys();
|
||||
console.log(active.length); // 1
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { InMemoryKeyStore } from "@suaveplan/jwt-tenant/jwks/in-memory-key-store";
|
||||
import { generateKeyPair } from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
|
||||
let time = 0;
|
||||
const clock = () => time;
|
||||
const store = new InMemoryKeyStore(clock);
|
||||
|
||||
// Add a key that expires at t=1000
|
||||
const kp = await generateKeyPair("EdDSA", { ttlMs: 1000, clock });
|
||||
await store.addKey(kp);
|
||||
|
||||
time = 500;
|
||||
console.log((await store.getActiveKeys()).length); // 1 — not yet expired
|
||||
|
||||
time = 2000;
|
||||
console.log((await store.getActiveKeys()).length); // 0 — expired
|
||||
|
||||
store.pruneExpired(); // remove expired entries from the Map
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `InMemoryKeyStore`
|
||||
|
||||
Constructor: `new InMemoryKeyStore(clock?)`.
|
||||
|
||||
- **`getActiveKeys(tenantId?): Promise<KeyPair[]>`** — Returns all non-expired key pairs. Expired keys (where `clock() >= expiresAt`) are excluded. The `tenantId` parameter is accepted for interface compatibility but is a no-op in this implementation.
|
||||
- **`getKeyById(keyId): Promise<KeyPair | null>`** — Looks up a key by its ID. Returns `null` when not found.
|
||||
- **`addKey(keyPair): Promise<void>`** — Inserts or replaces a key pair in the store.
|
||||
- **`removeKey(keyId): Promise<void>`** — Removes a key pair. No-op if the key doesn't exist.
|
||||
- **`pruneExpired(): void`** — Synchronously removes all expired entries. Call periodically to prevent unbounded memory growth in long-running processes.
|
||||
|
||||
### `createInMemoryKeyStore(clock?): InMemoryKeyStore`
|
||||
|
||||
Factory function; equivalent to `new InMemoryKeyStore(clock)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `keys` map is `protected` (not `private`) to allow test subclasses to inspect internal state without public API exposure. The `tenantId` parameter in `getActiveKeys` is explicitly voided (`void tenantId`) to satisfy the TypeScript "unused variable" checker while documenting the intentional no-op. In production multi-tenant scenarios, use a separate `InMemoryKeyStore` instance per tenant or implement a `DatabaseKeyStore` with tenant-scoped queries.
|
||||
@@ -0,0 +1,81 @@
|
||||
# jwks-endpoint
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a framework-agnostic JWKS endpoint backed by a `TenantKeyStore`. The endpoint serialises active public keys from the store into a `{ keys: JWK[] }` JSON Web Key Set and exposes an HTTP response builder that any framework can consume. An internal cache reduces key-store reads on high-traffic endpoints, with the TTL controlled by `cacheMaxAge`.
|
||||
|
||||
## Features
|
||||
|
||||
- `JWKSEndpoint` class with configurable cache TTL and injectable clock
|
||||
- `getJWKS(config?)` — fetches active public keys and returns a `JWKS` object
|
||||
- `handleRequest(request)` — returns a framework-agnostic `{ status, body, headers }` response
|
||||
- `clearCache()` — invalidates the internal JWKS cache on demand
|
||||
- `formatJWKSResponse(keys)` — standalone pure function to wrap JWK arrays
|
||||
- `createJWKSEndpoint(options)` — factory function
|
||||
- `includeExpired` option to bypass cache and include all keys
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { InMemoryKeyStore } from "@suaveplan/jwt-tenant/jwks/in-memory-key-store";
|
||||
import { JWKSEndpoint } from "@suaveplan/jwt-tenant/jwks/jwks-endpoint";
|
||||
import { generateKeyPair } from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
|
||||
const store = new InMemoryKeyStore();
|
||||
const kp = await generateKeyPair("EdDSA");
|
||||
await store.addKey(kp);
|
||||
|
||||
const endpoint = new JWKSEndpoint({ keyStore: store });
|
||||
const response = await endpoint.handleRequest(null);
|
||||
// response.status === 200
|
||||
// JSON.parse(response.body).keys.length === 1
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { JWKSEndpoint } from "@suaveplan/jwt-tenant/jwks/jwks-endpoint";
|
||||
|
||||
// Use in a Hono or Express route
|
||||
app.get("/.well-known/jwks.json", async (req, res) => {
|
||||
const response = await endpoint.handleRequest(req);
|
||||
res.status(response.status)
|
||||
.set(response.headers)
|
||||
.send(response.body);
|
||||
});
|
||||
|
||||
// Force cache invalidation after key rotation
|
||||
await store.addKey(newKeyPair);
|
||||
endpoint.clearCache();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `JWKSEndpoint`
|
||||
|
||||
Constructor: `new JWKSEndpoint(options: JWKSEndpointOptions)`.
|
||||
|
||||
- `options.keyStore` — `TenantKeyStore` implementation
|
||||
- `options.cacheMaxAge` — cache TTL in milliseconds (default 5 min)
|
||||
- `options.clock` — injectable clock for cache expiry
|
||||
|
||||
#### Methods
|
||||
|
||||
- **`getJWKS(config?): Promise<JWKS>`** — Returns active public keys as a JWKS. Caches result unless `config.includeExpired` is `true`.
|
||||
- **`handleRequest(request): Promise<JWKSResponse>`** — Returns `{ status: 200, body: string, headers }`. The `request` parameter is accepted for interface compatibility.
|
||||
- **`clearCache(): void`** — Resets the internal cache immediately.
|
||||
|
||||
### `formatJWKSResponse(keys: JWK[]): JWKS`
|
||||
|
||||
Pure function that wraps a `JWK[]` in a `{ keys }` envelope.
|
||||
|
||||
### `createJWKSEndpoint(options): JWKSEndpoint`
|
||||
|
||||
Factory function; equivalent to `new JWKSEndpoint(options)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The cache uses a simple timestamp check against `clock()`. Cache invalidation on key rotation must be explicit (`clearCache()`). The `handleRequest` `_request` parameter is intentionally ignored — the endpoint serves the same JWKS for all requests. In production, mount the endpoint behind a CDN with short TTLs for automatic key rotation propagation.
|
||||
@@ -0,0 +1,110 @@
|
||||
# key-generator
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides cryptographic key pair generation and serialization utilities for `@suaveplan/jwt-tenant`. Key generation delegates to the `jose` library for actual key material, while serialization converts between opaque `KeyPair` objects (with CryptoKey references) and JSON-safe `SerializedKeyPair` objects suitable for database storage or transmission. An injectable `Clock` makes timestamp generation deterministic in tests.
|
||||
|
||||
## Features
|
||||
|
||||
- `generateKeyPair(algorithm, options)` — generates ES256/ES384/ES512/EdDSA/RS256/RS384/RS512 key pairs
|
||||
- `serializeKeyPair(keyPair)` — exports keys to JWK format with metadata headers
|
||||
- `deserializeKeyPair(serialized)` — imports JWKs back to opaque key references
|
||||
- `keyPairToJWK(keyPair)` — extracts the public JWK for JWKS endpoint serving
|
||||
- `importPublicKeyFromJWK(jwk)` — imports a raw JWK for use in verification
|
||||
- `calculateKeyExpiration(createdAt, ttlMs)` — computes expiry Date from creation time and TTL
|
||||
- `isKeyPairExpired(keyPair, clock?)` — checks whether a key pair has passed its `expiresAt`
|
||||
- `getAlgorithmFamily(algorithm)` — returns `"EC" | "OKP" | "RSA"`
|
||||
- Injectable `Clock` for deterministic `createdAt`/`expiresAt` in tests
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateKeyPair, keyPairToJWK } from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
|
||||
const keyPair = await generateKeyPair("EdDSA");
|
||||
console.log(keyPair.keyId); // e.g. "550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
const jwk = await keyPairToJWK(keyPair);
|
||||
console.log(jwk.kty); // "OKP"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
generateKeyPair,
|
||||
serializeKeyPair,
|
||||
deserializeKeyPair,
|
||||
isKeyPairExpired,
|
||||
calculateKeyExpiration,
|
||||
} from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
|
||||
// Generate with 30-day TTL using a fixed clock for tests
|
||||
const clock = () => new Date("2025-01-01").getTime();
|
||||
const keyPair = await generateKeyPair("ES256", {
|
||||
keyId: "my-key-v1",
|
||||
ttlMs: 30 * 24 * 60 * 60 * 1000,
|
||||
clock,
|
||||
});
|
||||
|
||||
// Persist to storage
|
||||
const serialized = await serializeKeyPair(keyPair);
|
||||
localStorage.setItem("keyPair", JSON.stringify(serialized));
|
||||
|
||||
// Restore from storage
|
||||
const restored = await deserializeKeyPair(JSON.parse(localStorage.getItem("keyPair")!));
|
||||
console.log(restored.keyId === keyPair.keyId); // true
|
||||
|
||||
// Check expiry
|
||||
const expired = isKeyPairExpired(restored, () => Date.now());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generateKeyPair(algorithm?, options?): Promise<KeyPair>`
|
||||
|
||||
Generates a key pair for the given algorithm. Default algorithm is `"EdDSA"`.
|
||||
|
||||
- `options.keyId` — override the auto-generated UUID key ID
|
||||
- `options.ttlMs` — milliseconds until expiry; sets `expiresAt`
|
||||
- `options.clock` — injectable clock for `createdAt`/`expiresAt`
|
||||
- Throws `JwtTenantError(JWT.ALGORITHM_UNSUPPORTED)` on unknown algorithms
|
||||
|
||||
### `serializeKeyPair(keyPair): Promise<SerializedKeyPair>`
|
||||
|
||||
Exports a `KeyPair` to a JSON-serializable form with JWK-encoded keys.
|
||||
|
||||
- Throws `JwtTenantError(JWT.SERIALIZATION_FAILED)` on export failure
|
||||
|
||||
### `deserializeKeyPair(serialized): Promise<KeyPair>`
|
||||
|
||||
Imports a `SerializedKeyPair` back to an in-memory `KeyPair` with opaque key references.
|
||||
|
||||
- Throws `JwtTenantError(JWT.SERIALIZATION_FAILED)` on import failure
|
||||
|
||||
### `keyPairToJWK(keyPair): Promise<JWK>`
|
||||
|
||||
Returns the public JWK with `kid`, `alg`, and `use: "sig"` embedded.
|
||||
|
||||
### `importPublicKeyFromJWK(jwk): Promise<unknown>`
|
||||
|
||||
Imports a raw JWK as an opaque key reference for token verification.
|
||||
|
||||
### `calculateKeyExpiration(createdAt, ttlMs): Date`
|
||||
|
||||
Pure function — returns `createdAt + ttlMs` as a `Date`.
|
||||
|
||||
### `isKeyPairExpired(keyPair, clock?): boolean`
|
||||
|
||||
Returns `true` when `clock() >= keyPair.expiresAt.getTime()`. Returns `false` when `expiresAt` is absent.
|
||||
|
||||
### `getAlgorithmFamily(algorithm): "EC" | "OKP" | "RSA"`
|
||||
|
||||
Maps algorithm names to their key family for key generation routing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All `privateKey`/`publicKey` fields in `KeyPair` are typed as `unknown` to avoid leaking `jose` internal types into the public API. The `as Parameters<typeof exportJWK>[0]` casts are intentional adapter boundaries — they are not part of the public API contract. RSA key generation uses 2048-bit moduli for RS256, 3072 for RS384, and 4096 for RS512 to meet NIST recommendations.
|
||||
@@ -0,0 +1,77 @@
|
||||
# jwt-tenant.schemas
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for all public configuration and option types in `@suaveplan/jwt-tenant`. These schemas enable runtime validation of user-supplied configuration, making it possible to surface configuration errors at construction time rather than at operation time. They are also useful for serializing and deserializing key pairs in storage-layer integrations.
|
||||
|
||||
## Features
|
||||
|
||||
- `KeyGenOptionsSchema` — validates key generation options
|
||||
- `TokenIssuerConfigSchema` — validates issuer configuration
|
||||
- `CreateTokenOptionsSchema` — validates per-token creation options (required `subject` and `tenantId`)
|
||||
- `VerifyTokenOptionsSchema` — validates per-verification constraint options
|
||||
- `TokenVerifierConfigSchema` — validates verifier configuration
|
||||
- `JWKSEndpointOptionsSchema` — validates JWKS endpoint options
|
||||
- `SerializedKeyPairSchema` — validates serialized key pair JSON format
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CreateTokenOptionsSchema } from "@suaveplan/jwt-tenant/schemas";
|
||||
|
||||
const result = CreateTokenOptionsSchema.safeParse({
|
||||
subject: "user-123",
|
||||
tenantId: "acme",
|
||||
role: "admin",
|
||||
});
|
||||
if (!result.success) {
|
||||
console.error(result.error.format());
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SerializedKeyPairSchema } from "@suaveplan/jwt-tenant/schemas";
|
||||
|
||||
const rawJson = JSON.parse(fs.readFileSync("keypair.json", "utf8"));
|
||||
const parsed = SerializedKeyPairSchema.parse(rawJson);
|
||||
// parsed is type-narrowed as SerializedKeyPair
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `KeyGenOptionsSchema`
|
||||
|
||||
Strict schema: `keyId?` (string), `ttlMs?` (positive number).
|
||||
|
||||
### `TokenIssuerConfigSchema`
|
||||
|
||||
Strict schema: `issuer?` (URL string), `defaultExpiresIn?` (string).
|
||||
|
||||
### `CreateTokenOptionsSchema`
|
||||
|
||||
Strict schema with required `subject` (min 1) and `tenantId` (min 1). Optional: `expiresIn`, `domains`, `role`, `permissions`, `sessionId`, `issuer`, `additionalClaims`.
|
||||
|
||||
### `VerifyTokenOptionsSchema`
|
||||
|
||||
Strict schema: all fields optional — `requiredClaims`, `requiredRole`, `requiredPermissions`, `requiredTenantId`, `requiredDomains`.
|
||||
|
||||
### `TokenVerifierConfigSchema`
|
||||
|
||||
Strict schema: `issuer?` (URL), `audience?` (string[]), `clockTolerance?` (nonnegative), `jwksUri?` (URL), `cacheMaxAge?` (positive), `maxRetries?` (positive int).
|
||||
|
||||
### `JWKSEndpointOptionsSchema`
|
||||
|
||||
Strict schema: `cacheMaxAge?` (positive). The `keyStore` and `clock` fields are runtime objects and are excluded.
|
||||
|
||||
### `SerializedKeyPairSchema`
|
||||
|
||||
Validates `keyId`, `algorithm` (enum), `privateKeyJwk`/`publicKeyJwk` (records), `createdAt`/`expiresAt?` (ISO-8601 datetime strings).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All schemas use `.strict()` to reject unknown keys, making configuration typos immediately surfaced. The `clock` and `signal` fields in config interfaces are runtime objects that cannot be validated by Zod and are intentionally excluded from the schemas.
|
||||
@@ -0,0 +1,75 @@
|
||||
# token-issuer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Issues signed JWTs with per-tenant claims using the `jose` library. The `TokenIssuer` class wraps `SignJWT` with tenant-aware defaults: it injects `tenant_id`, `session_id`, `domains`, `role`, and `permissions` claims alongside standard `sub`, `iat`, `exp`, and `jti`. An injectable `Clock` makes token timestamps deterministic for unit tests. Duration strings like `"1h"` are parsed to seconds for `exp` computation.
|
||||
|
||||
## Features
|
||||
|
||||
- `TokenIssuer` class with configurable issuer, default expiry, and injectable clock
|
||||
- `createToken(keyPair, options)` — signs a JWT with tenant claims
|
||||
- `createTokenIssuer(config)` — factory function
|
||||
- `signToken(keyPair, options, config?)` — standalone convenience function
|
||||
- Duration string parsing: `"1h"`, `"15m"`, `"30s"`, `"2d"`, `"500ms"`
|
||||
- Standard `jti` (UUID), `iat`, `exp`, `sub`, `iss` claim population
|
||||
- Optional `additionalClaims` spread for domain-specific payloads
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateKeyPair } from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
import { TokenIssuer } from "@suaveplan/jwt-tenant/token/token-issuer";
|
||||
|
||||
const keyPair = await generateKeyPair("EdDSA");
|
||||
const issuer = new TokenIssuer({ issuer: "https://auth.example.com" });
|
||||
const token = await issuer.createToken(keyPair, {
|
||||
subject: "user-123",
|
||||
tenantId: "acme",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { signToken } from "@suaveplan/jwt-tenant/token/token-issuer";
|
||||
|
||||
const clock = () => new Date("2025-06-01T00:00:00Z").getTime();
|
||||
const token = await signToken(keyPair, {
|
||||
subject: "svc-account",
|
||||
tenantId: "internal",
|
||||
expiresIn: "15m",
|
||||
role: "service",
|
||||
permissions: ["read:metrics", "write:events"],
|
||||
sessionId: "sess-abc123",
|
||||
additionalClaims: { env: "production" },
|
||||
}, { clock });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `TokenIssuer`
|
||||
|
||||
Constructor: `new TokenIssuer(config?: TokenIssuerConfig)`.
|
||||
|
||||
- `config.issuer` — `iss` claim (optional)
|
||||
- `config.defaultExpiresIn` — default duration string (default `"1h"`)
|
||||
- `config.clock` — injectable clock for `iat`/`exp`
|
||||
|
||||
#### Methods
|
||||
|
||||
- **`createToken(keyPair, options): Promise<string>`** — Signs and returns a compact JWT.
|
||||
|
||||
### `createTokenIssuer(config?): TokenIssuer`
|
||||
|
||||
Factory function.
|
||||
|
||||
### `signToken(keyPair, options, config?): Promise<string>`
|
||||
|
||||
Standalone convenience that creates a `TokenIssuer` internally and issues one token.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Duration strings are parsed with a simple regex supporting `ms`, `s`, `m`, `h`, `d` units. Fractional values are not supported. The `exp` claim is set to `floor(now/1000) + duration_in_seconds`, matching the JWT spec's integer seconds requirement. The `jti` is a `crypto.randomUUID()` call for uniqueness.
|
||||
@@ -0,0 +1,83 @@
|
||||
# token-verifier
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Verifies signed JWTs against local keys or remote JWKS endpoints. The `TokenVerifier` class resolves verification keys from either an in-memory key cache or a remote JWKS URI (with retry and AbortSignal support), then delegates cryptographic verification to `jose`. After successful signature verification, per-verification constraint checks enforce tenant isolation, role requirements, and permission scopes.
|
||||
|
||||
## Features
|
||||
|
||||
- `TokenVerifier` class with configurable issuer, audience, clock tolerance, and injectable clock
|
||||
- Remote JWKS URI support with configurable TTL caching and retry (default 3 attempts)
|
||||
- `AbortSignal` cancellation for JWKS fetch operations
|
||||
- `addKey(jwk)` — register a local JWK for key-only verification (no remote fetch)
|
||||
- `clearCache()` — invalidate the JWKS cache for immediate rotation
|
||||
- `verifyToken(token, options?)` — returns `VerifyTokenResult` (never throws)
|
||||
- Per-verification constraints: `requiredTenantId`, `requiredRole`, `requiredPermissions`, `requiredClaims`, `requiredDomains`
|
||||
- `verifyTokenWithKey(token, key, options?)` — standalone function for pre-imported keys
|
||||
- `createTokenVerifier(config?)` — factory function
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateKeyPair, keyPairToJWK } from "@suaveplan/jwt-tenant/keys/key-generator";
|
||||
import { TokenIssuer } from "@suaveplan/jwt-tenant/token/token-issuer";
|
||||
import { TokenVerifier } from "@suaveplan/jwt-tenant/token/token-verifier";
|
||||
|
||||
const kp = await generateKeyPair("EdDSA");
|
||||
const issuer = new TokenIssuer({ issuer: "https://auth.example.com" });
|
||||
const token = await issuer.createToken(kp, { subject: "u1", tenantId: "acme" });
|
||||
|
||||
const verifier = new TokenVerifier({ issuer: "https://auth.example.com" });
|
||||
const jwk = await keyPairToJWK(kp);
|
||||
verifier.addKey(jwk);
|
||||
const result = await verifier.verifyToken(token);
|
||||
// result.valid === true
|
||||
// result.payload.tenant_id === "acme"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
const verifier = new TokenVerifier({
|
||||
jwksUri: "https://auth.example.com/.well-known/jwks.json",
|
||||
issuer: "https://auth.example.com",
|
||||
cacheMaxAge: 5 * 60 * 1000,
|
||||
clockTolerance: 30,
|
||||
maxRetries: 3,
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
const result = await verifier.verifyToken(token, {
|
||||
requiredTenantId: "acme",
|
||||
requiredRole: "admin",
|
||||
requiredPermissions: ["write:data"],
|
||||
requiredDomains: ["acme.example.com"],
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `TokenVerifier`
|
||||
|
||||
Constructor: `new TokenVerifier(config?: TokenVerifierConfig)`.
|
||||
|
||||
#### Methods
|
||||
|
||||
- **`verifyToken(token, options?): Promise<VerifyTokenResult>`** — Verifies the token. Returns `{ valid: true, payload }` or `{ valid: false, error, errorCode }`. Never throws.
|
||||
- **`addKey(jwk): void`** — Adds a local JWK to the key cache.
|
||||
- **`clearCache(): void`** — Clears the key cache.
|
||||
|
||||
### `createTokenVerifier(config?): TokenVerifier`
|
||||
|
||||
Factory function.
|
||||
|
||||
### `verifyTokenWithKey(token, key, options?): Promise<VerifyTokenResult>`
|
||||
|
||||
Standalone verification with a pre-imported key reference.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
JWKS cache uses timestamp comparison (`clock() >= cacheExpiresAt`) for expiry. Multi-key verification tries each JWK in order and returns on the first success — the last error is preserved and wrapped if all keys fail. The `checkConstraints` method is `protected` to allow subclass extension without exposing it in the public API. All jose errors are classified by message substring matching into one of the JWT error codes.
|
||||
102
docs/identity/auth/jwt-tenant/src/types/jwt-tenant.types.md
Normal file
102
docs/identity/auth/jwt-tenant/src/types/jwt-tenant.types.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# jwt-tenant.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the complete public type surface for `@suaveplan/jwt-tenant`. Every interface and type alias used across the package is declared here: key pair shapes, JWT payload structure, issuer/verifier configuration, JWKS endpoint types, and the `TenantKeyStore` adapter interface. Centralising types in one file avoids circular imports and makes the public contract immediately readable.
|
||||
|
||||
## Features
|
||||
|
||||
- `KeyAlgorithm` union covering ECDSA (ES256/ES384/ES512), EdDSA, and RSA (RS256/RS384/RS512)
|
||||
- `KeyPair` / `SerializedKeyPair` for in-memory and JSON-exportable key representations
|
||||
- `JWK` / `JWKS` RFC 7517-compatible web key types with open index signature
|
||||
- `TenantJWTPayload` with required `tenant_id`, `sub`, `iat`, `exp`, `jti` plus optional tenant claims
|
||||
- `TokenIssuerConfig` / `CreateTokenOptions` for token creation
|
||||
- `TokenVerifierConfig` / `VerifyTokenOptions` / `VerifyTokenResult` for verification
|
||||
- `KeyGenOptions` with injectable `Clock` for deterministic timestamps
|
||||
- `JWKSEndpointOptions` / `JWKSEndpointConfig` / `JWKSResponse` for JWKS serving
|
||||
- `TenantKeyStore` adapter interface for pluggable key storage
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
KeyPair,
|
||||
TenantJWTPayload,
|
||||
TokenIssuerConfig,
|
||||
CreateTokenOptions,
|
||||
} from "@suaveplan/jwt-tenant";
|
||||
|
||||
const config: TokenIssuerConfig = {
|
||||
issuer: "https://auth.example.com",
|
||||
defaultExpiresIn: "1h",
|
||||
};
|
||||
|
||||
const options: CreateTokenOptions = {
|
||||
subject: "user-123",
|
||||
tenantId: "acme",
|
||||
role: "admin",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
TenantKeyStore,
|
||||
KeyPair,
|
||||
JWKSEndpointOptions,
|
||||
TokenVerifierConfig,
|
||||
} from "@suaveplan/jwt-tenant";
|
||||
|
||||
// Implement a custom key store backed by a database
|
||||
class DatabaseKeyStore implements TenantKeyStore {
|
||||
async getActiveKeys(tenantId?: string): Promise<KeyPair[]> {
|
||||
// fetch from DB filtered by tenantId + expiry
|
||||
return [];
|
||||
}
|
||||
async getKeyById(keyId: string): Promise<KeyPair | null> {
|
||||
// SELECT by keyId
|
||||
return null;
|
||||
}
|
||||
async addKey(keyPair: KeyPair): Promise<void> {
|
||||
// INSERT
|
||||
}
|
||||
async removeKey(keyId: string): Promise<void> {
|
||||
// DELETE
|
||||
}
|
||||
}
|
||||
|
||||
const verifierConfig: TokenVerifierConfig = {
|
||||
issuer: "https://auth.example.com",
|
||||
clockTolerance: 30,
|
||||
jwksUri: "https://auth.example.com/.well-known/jwks.json",
|
||||
cacheMaxAge: 5 * 60 * 1000,
|
||||
maxRetries: 3,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `KeyAlgorithm`
|
||||
|
||||
Union type: `"ES256" | "ES384" | "ES512" | "EdDSA" | "RS256" | "RS384" | "RS512"`.
|
||||
|
||||
### `KeyPair`
|
||||
|
||||
In-memory key pair with opaque `privateKey`/`publicKey` fields (CryptoKey or jose KeyLike).
|
||||
|
||||
### `SerializedKeyPair`
|
||||
|
||||
JSON-exportable form with `privateKeyJwk`/`publicKeyJwk` as JWK objects and ISO-8601 date strings.
|
||||
|
||||
### `TenantJWTPayload`
|
||||
|
||||
Standard JWT claims plus `tenant_id`, optional `domains`, `role`, `permissions`, `session_id`. Open index signature allows additional claims.
|
||||
|
||||
### `TenantKeyStore`
|
||||
|
||||
Pluggable storage adapter: `getActiveKeys(tenantId?)`, `getKeyById(id)`, `addKey(kp)`, `removeKey(id)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All `privateKey` and `publicKey` fields are typed as `unknown` to avoid exposing the `jose` library's internal types in the public interface. Consumers that need raw key material should use `serializeKeyPair` / `deserializeKeyPair` instead.
|
||||
Reference in New Issue
Block a user