fix: encrypt OIDC client secret at rest using AES-256-GCM

The oidc_client_secret was written to app_settings as plaintext,
unlike Maps and OpenWeather API keys which are protected with
apiKeyCrypto. An attacker with read access to the SQLite file
(e.g. via a backup download) could obtain the secret and
impersonate the application with the identity provider.

- Encrypt on write in PUT /api/admin/oidc via maybe_encrypt_api_key
- Decrypt on read in GET /api/admin/oidc and in getOidcConfig()
  (oidc.ts) before passing the secret to the OIDC client library
- Add a startup migration that encrypts any existing plaintext value
  already present in the database
This commit is contained in:
jubnl
2026-04-01 04:27:50 +02:00
parent 701a8ab03a
commit bba50f038b
3 changed files with 13 additions and 3 deletions
+2 -1
View File
@@ -5,6 +5,7 @@ import jwt from 'jsonwebtoken';
import { db } from '../db/database';
import { JWT_SECRET } from '../config';
import { User } from '../types';
import { decrypt_api_key } from '../services/apiKeyCrypto';
interface OidcDiscoveryDoc {
authorization_endpoint: string;
@@ -57,7 +58,7 @@ function getOidcConfig() {
const get = (key: string) => (db.prepare("SELECT value FROM app_settings WHERE key = ?").get(key) as { value: string } | undefined)?.value || null;
const issuer = process.env.OIDC_ISSUER || get('oidc_issuer');
const clientId = process.env.OIDC_CLIENT_ID || get('oidc_client_id');
const clientSecret = process.env.OIDC_CLIENT_SECRET || get('oidc_client_secret');
const clientSecret = process.env.OIDC_CLIENT_SECRET || decrypt_api_key(get('oidc_client_secret'));
const displayName = process.env.OIDC_DISPLAY_NAME || get('oidc_display_name') || 'SSO';
if (!issuer || !clientId || !clientSecret) return null;
return { issuer: issuer.replace(/\/+$/, ''), clientId, clientSecret, displayName };