Going further

Signing keys

Give OAuth Server the private key it signs tokens with, per environment, and rotate it without signing anyone out.

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.

For: whoever deploys an app that runs OAuth Server, including your own issuer.

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

Keep the private key out of git, and delete the local file once the secret is stored. Anyone with this key can mint tokens for every application. Use a different key per environment.

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
The key must be in PKCS#8 format (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-----"
What you should see:/.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_KEY nor runtimeConfig.oauthServer.privateKey is 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.

If a private key leaked, do not follow these steps. Keeping it in 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.
  1. Generate a new key pair, as above.
  2. Move the old public key into previousPublicKeys, keyed by its old keyId.
  3. Switch to the new private key and a new keyId.
  4. Deploy. The JWKS lists both keys. Apps that cache it pick up the new key when they meet an unknown kid.
  5. 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
  },
})

Next steps

Copyright © 2026