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:
@@ -1,4 +1,5 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { encrypt_api_key } from '../services/apiKeyCrypto';
|
||||
|
||||
function runMigrations(db: Database.Database): void {
|
||||
db.exec('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)');
|
||||
@@ -448,6 +449,13 @@ function runMigrations(db: Database.Database): void {
|
||||
() => {
|
||||
try { db.exec('ALTER TABLE trips ADD COLUMN reminder_days INTEGER DEFAULT 3'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
|
||||
},
|
||||
// Encrypt any plaintext oidc_client_secret left in app_settings
|
||||
() => {
|
||||
const row = db.prepare("SELECT value FROM app_settings WHERE key = 'oidc_client_secret'").get() as { value: string } | undefined;
|
||||
if (row?.value && !row.value.startsWith('enc:v1:')) {
|
||||
db.prepare("UPDATE app_settings SET value = ? WHERE key = 'oidc_client_secret'").run(encrypt_api_key(row.value));
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if (currentVersion < migrations.length) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AuthRequest, User, Addon } from '../types';
|
||||
import { writeAudit, getClientIp, logInfo } from '../services/auditLog';
|
||||
import { getAllPermissions, savePermissions, PERMISSION_ACTIONS } from '../services/permissions';
|
||||
import { revokeUserSessions } from '../mcp';
|
||||
import { maybe_encrypt_api_key, decrypt_api_key } from '../services/apiKeyCrypto';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -232,7 +233,7 @@ router.get('/audit-log', (req: Request, res: Response) => {
|
||||
|
||||
router.get('/oidc', (_req: Request, res: Response) => {
|
||||
const get = (key: string) => (db.prepare("SELECT value FROM app_settings WHERE key = ?").get(key) as { value: string } | undefined)?.value || '';
|
||||
const secret = get('oidc_client_secret');
|
||||
const secret = decrypt_api_key(get('oidc_client_secret'));
|
||||
res.json({
|
||||
issuer: get('oidc_issuer'),
|
||||
client_id: get('oidc_client_id'),
|
||||
@@ -247,7 +248,7 @@ router.put('/oidc', (req: Request, res: Response) => {
|
||||
const set = (key: string, val: string) => db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)").run(key, val || '');
|
||||
set('oidc_issuer', issuer);
|
||||
set('oidc_client_id', client_id);
|
||||
if (client_secret !== undefined) set('oidc_client_secret', client_secret);
|
||||
if (client_secret !== undefined) set('oidc_client_secret', maybe_encrypt_api_key(client_secret) ?? '');
|
||||
set('oidc_display_name', display_name);
|
||||
set('oidc_only', oidc_only ? 'true' : 'false');
|
||||
const authReq = req as AuthRequest;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user