deploy: POST /api/wallets + full swagger
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import * as jose from 'jose';
|
||||
import { env } from '../config/env';
|
||||
import { fetchVaultKV2 } from '../config/vault';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
export interface AccessTokenPayload {
|
||||
@@ -19,21 +20,41 @@ export interface AuthContext {
|
||||
token: AccessTokenPayload;
|
||||
}
|
||||
|
||||
const keyMap = new Map<string, Awaited<ReturnType<typeof jose.importSPKI>>>();
|
||||
type KeyType = Awaited<ReturnType<typeof jose.importSPKI>>;
|
||||
|
||||
export async function loadJwtKeysFromVault(
|
||||
// Whitelist надёжных асимметричных алгоритмов. Никогда не разрешаем 'none'/HS*
|
||||
// (HS — симметричные, могли бы быть подставлены через algorithm confusion).
|
||||
const ALLOWED_ALGORITHMS = new Set(['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'EdDSA', 'PS256', 'PS384', 'PS512']);
|
||||
|
||||
if (!ALLOWED_ALGORITHMS.has(env.jwt.algorithm)) {
|
||||
throw new Error(`JWT_ALGORITHM "${env.jwt.algorithm}" not allowed. Use one of: ${[...ALLOWED_ALGORITHMS].join(', ')}`);
|
||||
}
|
||||
|
||||
// Live key store — атомарно подменяется через swapKeyMap()
|
||||
let keyMap: Map<string, KeyType> = new Map();
|
||||
|
||||
export function swapKeyMap(newMap: Map<string, KeyType>): void {
|
||||
keyMap = newMap;
|
||||
}
|
||||
|
||||
export function getKeyMapSize(): number {
|
||||
return keyMap.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fetch JWT public keys from Vault, не мутируя глобальный keyMap.
|
||||
* Возвращает новую Map для атомарного swap'а.
|
||||
*/
|
||||
export async function fetchJwtKeysFromVault(
|
||||
vaultAddr: string,
|
||||
vaultToken: string,
|
||||
mount: string,
|
||||
kidPath: string,
|
||||
kidsPrefix: string,
|
||||
): Promise<void> {
|
||||
const { fetchVaultKV2 } = await import('../config/vault');
|
||||
|
||||
): Promise<Map<string, KeyType>> {
|
||||
const kidData = await fetchVaultKV2(vaultAddr, vaultToken, mount, kidPath);
|
||||
if (!kidData) {
|
||||
logger.warn('Failed to read JWT kid config from Vault');
|
||||
return;
|
||||
throw new Error('Failed to read JWT kid config from Vault');
|
||||
}
|
||||
|
||||
const kids: string[] = [];
|
||||
@@ -41,10 +62,11 @@ export async function loadJwtKeysFromVault(
|
||||
if (kidData.previous && kidData.previous !== kidData.active) kids.push(kidData.previous);
|
||||
|
||||
if (kids.length === 0) {
|
||||
logger.warn('No active/previous kids found in Vault');
|
||||
return;
|
||||
throw new Error('No active/previous kids found in Vault');
|
||||
}
|
||||
|
||||
const next = new Map<string, KeyType>();
|
||||
|
||||
for (const kid of kids) {
|
||||
const kidSecret = await fetchVaultKV2(vaultAddr, vaultToken, mount, `${kidsPrefix}/${kid}`);
|
||||
if (!kidSecret?.public_key) {
|
||||
@@ -52,16 +74,15 @@ export async function loadJwtKeysFromVault(
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const key = await jose.importSPKI(kidSecret.public_key, env.jwt.algorithm);
|
||||
keyMap.set(kid, key);
|
||||
logger.info(`Loaded JWT public key for kid=${kid}`);
|
||||
} catch (err: any) {
|
||||
logger.error(`Failed to import public key for kid=${kid}: ${err.message}`);
|
||||
}
|
||||
const key = await jose.importSPKI(kidSecret.public_key, env.jwt.algorithm);
|
||||
next.set(kid, key);
|
||||
}
|
||||
|
||||
logger.info(`JWT key store loaded: ${keyMap.size} key(s)`);
|
||||
if (next.size === 0) {
|
||||
throw new Error('No public keys could be loaded from Vault');
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function verifyAccessToken(token: string): Promise<AuthContext> {
|
||||
@@ -71,17 +92,17 @@ export async function verifyAccessToken(token: string): Promise<AuthContext> {
|
||||
const header = jose.decodeProtectedHeader(token);
|
||||
const kid = header.kid;
|
||||
|
||||
if (!kid) {
|
||||
throw Object.assign(new Error('Missing kid in token header'), { status: 401 });
|
||||
if (!kid || typeof kid !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/.test(kid)) {
|
||||
throw Object.assign(new Error('Missing or invalid kid in token header'), { status: 401 });
|
||||
}
|
||||
|
||||
const key = keyMap.get(kid);
|
||||
if (!key) {
|
||||
logger.warn(`Unknown kid=${kid}`);
|
||||
throw Object.assign(new Error('Unknown token kid'), { status: 401 });
|
||||
}
|
||||
|
||||
if (header.alg !== env.jwt.algorithm) {
|
||||
// Двойная защита от algorithm confusion: проверяем точное совпадение
|
||||
if (header.alg !== env.jwt.algorithm || !ALLOWED_ALGORITHMS.has(String(header.alg))) {
|
||||
throw Object.assign(new Error('Invalid token algorithm'), { status: 401 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user