diff --git a/README.md b/README.md index 3f2f803..24dd53e 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ REDIS_PORT=6379 # Session Configuration SESSION_COOKIE_NAME=connect.sid SESSION_SECRET=your-session-secret +# Optional: override derived state signing secret +# STATE_SIGNING_SECRET=your-state-secret # RP Configuration RP_FRONTEND_URL=http://localhost:3000 @@ -110,6 +112,37 @@ COOKIE_DOMAIN=localhost OIDC_REDIRECT_BASE_URL=http://localhost:3668 ``` +### Generating strong secrets + +Use the helper script to produce high-entropy values for +`SESSION_SECRET` and `STATE_SIGNING_SECRET`: + +```bash +npm run generate:secrets +``` + +You can also invoke the packaged CLI from your own project: + +```bash +npx swissoid-back-generate-secrets +``` + +Example output: + +``` +SESSION_SECRET=8Qd8d...snipped +STATE_SIGNING_SECRET=Ob7v3...snipped + +# Copy the values above into your deployment secret store (.env, Vault, etc.). +# Keep them private and rotate on a regular schedule. +``` + +- Pass `--derive-state` to derive the state secret from the session secret + (mirrors the default behaviour when `STATE_SIGNING_SECRET` is omitted). +- Adjust entropy with `--session-bytes=` or `--state-bytes=` if you need + different lengths (defaults: 48 bytes for the session secret and 32 for the + state secret). + ## Routes The package provides the following OIDC routes when loaded: diff --git a/package.json b/package.json index 18f5500..5d5db81 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,11 @@ "build": "tsc", "dev": "tsc --watch", "test": "jest", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "generate:secrets": "node scripts/generate-session-secrets.js" + }, + "bin": { + "swissoid-back-generate-secrets": "./scripts/generate-session-secrets.js" }, "keywords": [ "swissoid", @@ -41,6 +45,7 @@ "typescript": "^5.9.2" }, "files": [ - "dist" + "dist", + "scripts/generate-session-secrets.js" ] } diff --git a/scripts/generate-session-secrets.js b/scripts/generate-session-secrets.js new file mode 100755 index 0000000..9218c9e --- /dev/null +++ b/scripts/generate-session-secrets.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/** + * Helper CLI to generate SESSION_SECRET and STATE_SIGNING_SECRET values. + * + * Usage: + * node scripts/generate-session-secrets.js + * + * Optional flags: + * --session-bytes= (default: 48) + * --state-bytes= (default: 32) + * --derive-state (derive STATE_SIGNING_SECRET from SESSION_SECRET) + */ + +const { randomBytes } = require('node:crypto'); + +const defaults = { + sessionBytes: 48, + stateBytes: 32, + deriveState: false, +}; + +const options = process.argv.slice(2).reduce((acc, arg) => { + if (arg.startsWith('--session-bytes=')) { + acc.sessionBytes = parseInt(arg.split('=')[1], 10); + } else if (arg.startsWith('--state-bytes=')) { + acc.stateBytes = parseInt(arg.split('=')[1], 10); + } else if (arg === '--derive-state') { + acc.deriveState = true; + } + return acc; +}, { ...defaults }); + +const validateBytes = (value, flagName) => { + if (!Number.isInteger(value) || value <= 0) { + console.error(`Invalid value for ${flagName}. Expected a positive integer, received: ${value}`); + process.exit(1); + } +}; + +validateBytes(options.sessionBytes, '--session-bytes'); +if (!options.deriveState) { + validateBytes(options.stateBytes, '--state-bytes'); +} + +const generateSecret = (bytes) => randomBytes(bytes).toString('base64'); + +const sessionSecret = generateSecret(options.sessionBytes); +const stateSecret = options.deriveState + ? `${sessionSecret}-state-signing` + : generateSecret(options.stateBytes); + +console.log(`SESSION_SECRET=${sessionSecret}`); +console.log(`STATE_SIGNING_SECRET=${stateSecret}`); + +console.log('\n# Copy the values above into your deployment secret store (.env, Vault, etc.).'); +console.log('# Keep them private and rotate on a regular schedule.');