Signing keys
Every token your issuer hands out is signed, so the apps and services that receive it can trust it. A production issuer has no key until you give it one, and a leaked key lets anyone mint tokens. This page generates a key, stores it for each environment and rotates it without signing anyone out.
How signing works
OAuth Server signs ID tokens and access tokens with an RSA private key, using RS256 (RSA with SHA-256). It derives the
matching public key from the private key. It publishes that public key as a JSON Web Key Set (JWKS) at
/.well-known/jwks.json. OAuth applications and services use it to verify tokens.
Generate and store a key
Generate a key pair
Generate one with OpenSSL:
# 2048-bit RSA private key, PKCS#8 (-----BEGIN PRIVATE KEY-----)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out oauth-private.pem
# Optional: the matching public key, e.g. to compare with the JWKS
openssl pkey -in oauth-private.pem -pubout -out oauth-public.pem
Without OpenSSL, Node.js generates the same thing:
node -e "const {generateKeyPairSync}=require('node:crypto');const k=generateKeyPairSync('rsa',{modulusLength:2048,privateKeyEncoding:{type:'pkcs8',format:'pem'},publicKeyEncoding:{type:'spki',format:'pem'}});console.log(k.privateKey)" > oauth-private.pem
BEGIN PRIVATE KEY). PKCS#1 (BEGIN RSA PRIVATE KEY) is rejected. Convert it with
openssl pkcs8 -topk8 -nocrypt -in old.pem -out oauth-private.pem.Turn it into one line
Environment variables are single-line, so newlines are written as a literal \n. Wrapping quotes are stripped. This
prints the value, ready to paste:
awk 'NF {printf "%s\\n", $0}' oauth-private.pem
Store it in each environment
Set NUXT_OAUTH_SERVER_PRIVATE_KEY wherever the app runs:
NUXT_OAUTH_SERVER_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkq...\n-----END PRIVATE KEY-----"
# Stored as an encrypted secret on the worker, not in wrangler.json
awk 'NF {printf "%s\\n", $0}' oauth-private.pem | pnpm dlx wrangler secret put NUXT_OAUTH_SERVER_PRIVATE_KEY
# Repository secret, passed to the deploy step as NUXT_OAUTH_SERVER_PRIVATE_KEY
awk 'NF {printf "%s\\n", $0}' oauth-private.pem | gh secret set NUXT_OAUTH_SERVER_PRIVATE_KEY
/.well-known/jwks.json returns one key, with kty: "RSA", your kid and no d field.A missing or malformed key answers 500 "The OAuth server signing keys are not configured."
Development key
You need no key under nuxt dev. When no private key is configured, the module generates one into
.data/oauth-server/dev-key.pem (mode 0600) the first time. It reuses that key on every restart, with a warning in
the console.
- "No private key" means neither
NUXT_OAUTH_SERVER_PRIVATE_KEYnorruntimeConfig.oauthServer.privateKeyis set. - Sign-in works out of the box, and tokens survive restarts.
- Delete the file to get a new key.
- If the file cannot be written, the module warns, and the dev server behaves as it does without a key.
nuxt build never generates a key. A production server without one keeps answering 500 not_configured.Rotate a key
Rotation has an overlap period. New tokens are signed with the new key. The old key's public half stays in the JWKS, and keeps verifying tokens already issued. Nobody is signed out. Refresh tokens are opaque, so rotation does not affect them.
previousPublicKeys would keep its tokens valid
for the overlap period. Instead, replace the key, leave previousPublicKeys empty, and use Sign everyone out on
each application in the admin area.- Generate a new key pair, as above.
- Move the old public key into
previousPublicKeys, keyed by its oldkeyId. - Switch to the new private key and a new
keyId. - Deploy. The JWKS lists both keys. Apps that cache it pick up the new key when they meet an unknown
kid. - Wait for the longest token lifetime:
accessTokenTtl, plus the time apps cache your JWKS. Then remove the old entry.
This prints the public key you are retiring, as a JSON object keyed by its kid, ready for an environment variable:
# The public key of the key you are retiring, as a JSON object keyed by its kid (single line, ready for an env var)
node -e "console.log(JSON.stringify({ 'key-1': require('fs').readFileSync('oauth-public-old.pem','utf8') }))"
export default defineNuxtConfig({
oauthServer: {
keyId: 'key-2', // the new key
},
})
NUXT_OAUTH_SERVER_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n…new key…\n-----END PRIVATE KEY-----"
# The retired key: public half only, nothing is signed with it any more
NUXT_OAUTH_SERVER_PREVIOUS_PUBLIC_KEYS='{"key-1":"-----BEGIN PUBLIC KEY-----\n…old key…\n-----END PUBLIC KEY-----\n"}'
Next steps
- Create an OAuth application – register the apps that verify these tokens.
- Deployment – set the key as a secret on your hosting platform.
- OAuth Server – look up the
keyIdandpreviousPublicKeysoptions.
Create an OAuth application
Register an app at an issuer that runs OAuth Server, as a confidential application in the admin area or a public one in the admin area or nuxt.config.
Stripe setup
Set up the Stripe dashboard, webhook endpoint, keys and KV once, so Stripe, Payments and Subscriptions all work, in production and locally.