Skip to main content

On This Page

Implementing Persistent JWT Signing Keys with PostgreSQL and Envelope Encryption

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Persistent JWT Signing Keys with PostgreSQL

The @saurbit/oauth2-jwt system provides high-security key management by separating public and private key storage interfaces. In-memory stores fail in production as server restarts or multiple instances invalidate all previously issued tokens.

Why This Matters

While tutorials often use in-memory stores for simplicity, production systems face the reality of ephemeral server instances and horizontal scaling. Storing private keys in plaintext or failing to synchronize keys across pods creates immediate authentication failures; envelope encryption bridges this gap by securing the database layer while ensuring high availability.

Key Insights

  • Envelope Encryption uses a random 256-bit DEK per key pair, which is then wrapped by a master KEK stored only in environment variables.
  • Database persistence ensures that tokens issued before a server restart or across different pods remain verifiable by resource servers.
  • Key rotation is automated by the JwksRotator which checks the created_at timestamp of the active private key record.
  • Overlap periods are managed by retaining expired public keys in the public_keys table until their TTL is reached, allowing for seamless rotation.

Working Examples

PostgreSQL schema for persistent key storage including a single-row constraint for active private keys.

CREATE TABLE IF NOT EXISTS private_keys (key_id character varying(255) NOT NULL, id integer NOT NULL DEFAULT 1, private_key text NOT NULL, wrapped_dek text NOT NULL, expires_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL DEFAULT now(), CONSTRAINT private_keys_pkey PRIMARY KEY (key_id), CONSTRAINT private_keys_id_unique UNIQUE (id), CONSTRAINT id CHECK (id = 1)); CREATE TABLE IF NOT EXISTS public_keys (key_id character varying(255) NOT NULL, public_key text NOT NULL, expires_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL DEFAULT now(), CONSTRAINT public_keys_pkey PRIMARY KEY (key_id));

Implementation of JwksKeyStore using envelope encryption (AES-256-GCM) with a Data Encryption Key (DEK) and Master Key (KEK).

export const jwksStore: JwksKeyStore = { async storeKeyPair(kid: string, privateKey: object, publicKey: object, ttl: number) { const dekRaw = crypto.getRandomValues(new Uint8Array(32)); const encryptedPrivateKey = await encrypt(JSON.stringify(privateKey), dekRaw); const wrappedDek = await encrypt(uint8ArrayToBase64(dekRaw), MASTER_KEY_RAW); const expirationTime = Date.now() + ttl * 1000; const privateKeyRecord: PrivateKeyRecord = { keyId: kid, privateKey: uint8ArrayToBase64(encryptedPrivateKey), wrappedDek: uint8ArrayToBase64(wrappedDek) }; const publicKeyRecord: PublicKeyRecord = { keyId: kid, publicKey: JSON.stringify(publicKey) }; await saveKeyPairRecord(privateKeyRecord, publicKeyRecord, expirationTime); } };

Practical Applications

  • Multi-instance OIDC servers: Using a shared PostgreSQL database ensures token consistency across pods. Pitfall: Hardcoding the MASTER_KEY in source control leads to total system compromise if the repository is leaked.
  • Automated key rotation: Systems using JoseJwksAuthority generate new RSA pairs every 91 days while maintaining old public keys for verification. Pitfall: Setting rotation intervals longer than public key TTLs causes a verification gap.

References:

Continue reading

Next article

Essential Observability: 3 Critical Alerts for LLM Systems

Related Content