A Flows app that generates OIDC-compliant JWT tokens with automatic key rotation and invalidation capabilities.
This app provides a complete JWT issuer service with:
- Dynamic Key Rotation: New signing keys generated based on token lifetime (half-interval)
- Token Invalidation: Change keyring to instantly invalidate all existing tokens
- OIDC Compliance: Standards-compliant discovery and JWKS endpoints
- Configuration Reactivity: Tokens regenerated when config changes
main.ts # App definition and core lifecycle logic
src/
types.ts # TypeScript interfaces and constants
crypto.ts # Key generation, JWT creation, config hashing
handlers.ts # HTTP handlers for OIDC endpoints
When the app first starts or config changes:
- Config Analysis: Hash current config to detect changes
- Token Check: Look for existing token in KV store
- Decision Logic:
- No token: Generate initial key pair and token
- Config changed: Regenerate everything with new config
- No changes: Reuse existing token
- Signal Updates: Update all signals including the active keyring
Automatic rotation using timers based on token lifetime:
- Interval Calculation: Rotation every
Math.max(5, tokenLifetime / 2)
minutes - Timer Management: Each rotation sets next timer and stores timer ID for cancellation
- Key Generation: Generate new RSA key pair and JWT token
- Signal Updates: Trigger sync to update all signals with new token
- Config Changes: Cancel existing timers and reset with new intervals
Problem: If keyring config changes, JWKS endpoint might use new keyring before keys exist.
Solution: Keyring signal pattern
- Config keyring = user's desired keyring
- Signal keyring = currently active keyring (only updated after successful key generation)
- JWKS endpoint uses signal keyring only
- If no signal keyring exists, return empty JWKS (safe fallback)
Keyring Change Flow:
- User changes
keyring
config from "default" to "v2" - Next sync detects config change
- Generates new keys under prefix
key:v2:
- Updates keyring signal to "v2"
- Old tokens become invalid (keys under
key:default:
no longer served by JWKS)
Field | Type | Description |
---|---|---|
expirationMinutes |
number | Token validity duration in minutes (minimum: 10, default: 120) |
audience |
string | Token audience claim (optional) |
additionalClaims |
object | Custom claims to include (optional) |
keyring |
string | Key ring ID for token invalidation (default: "default") |
Signal | Description |
---|---|
token |
Current JWT token (sensitive) |
expiresAt |
Token expiration Unix timestamp |
issuer |
OIDC issuer URL |
keyring |
Currently active keyring ID |
OIDC discovery endpoint with:
- Issuer information
- JWKS URI
- Supported algorithms
- Claims information
JSON Web Key Set endpoint:
- Serves all active public keys
- Paginates through KV store
- Only serves keys from signal keyring (race condition safe)
- Returns empty set if no keyring signal exists
Key Prefix Pattern: key:{keyring}:{keyId}
Examples:
- Default keyring:
key:default:uuid-1234
- Custom keyring:
key:production:uuid-5678
TTL Calculation: grace_period + max_token_lifetime
- Grace period: 30 minutes (configurable buffer)
- Ensures tokens remain verifiable throughout their lifetime
- Keys cleaned up automatically to prevent accumulation
Uses SHA-256 hash of:
{
"expirationMinutes": 120,
"audience": "api.example.com",
"additionalClaims": {...},
"keyring": "default"
}
Stored with each token to detect when regeneration is needed.
- Private Key Lifecycle: Generated, used once for signing, then discarded
- Public Key TTL: Automatic cleanup prevents key accumulation
- Race Condition Safety: JWKS only serves keys that are guaranteed to exist
- Instant Invalidation: Keyring changes immediately invalidate tokens
- Minimum Token Lifetime: 10-minute minimum prevents security issues
- Dynamic Rotation: Timer intervals adapt to token lifetime for optimal security
- OIDC Compliance: Standards-compliant for broad compatibility
The app follows TypeScript community standards:
src/
directory for source code- Clear separation of concerns
- Comprehensive error handling
- Extensive logging for debugging
- Install the app in your flow
- Configure token settings
- Use
token
andissuer
signals in your blocks - Point token validators to
{appUrl}/.well-known/openid-configuration
- Change
keyring
value to invalidate all existing tokens