feat: мок JWT-аутентификация (RS256, JWKS, cookie)
- src/auth.js: генерация ключей RS256, JWKS endpoint, verify/issue/middleware - views/login.ejs: мок-страница входа с выбором пользователя - server.js: cookie-parser, /login, /logout, auth.middleware - При переключении на прод: JWKS_URL в env → всё остальное без изменений
This commit is contained in:
+135
@@ -0,0 +1,135 @@
|
||||
const jose = require('jose');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// ── Конфигурация ──
|
||||
const ISSUER = process.env.JWT_ISSUER || 'mock-auth-api';
|
||||
const JWKS_URL = process.env.JWKS_URL || ''; // пусто → локальные ключи
|
||||
|
||||
let keyPair = null; // { publicKey, privateKey } — CryptoKeyPair
|
||||
let jwks = null; // предсобранный JWKS
|
||||
|
||||
// ── Инициализация ──
|
||||
|
||||
async function initAuth() {
|
||||
// Если задан внешний JWKS — используем его (продакшен)
|
||||
if (JWKS_URL) {
|
||||
console.log(`🔐 Auth: внешний JWKS ${JWKS_URL}`);
|
||||
return { verify: verifyJWT, middleware: createMiddleware(), jwksHandler: null };
|
||||
}
|
||||
|
||||
// Mock: генерируем локальную ключевую пару
|
||||
console.log('🔐 Auth: MOCK — локальный ключ RS256');
|
||||
keyPair = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
modulusLength: 2048,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
true,
|
||||
['sign', 'verify']
|
||||
);
|
||||
|
||||
const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||
jwks = { keys: [{ ...publicJwk, alg: 'RS256', use: 'sig', kid: 'mock-1' }] };
|
||||
|
||||
return {
|
||||
verify: verifyJWT,
|
||||
issue: issueJWT,
|
||||
middleware: createMiddleware(),
|
||||
jwksHandler: (req, res) => res.json(jwks),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Middleware ──
|
||||
|
||||
function createMiddleware() {
|
||||
return async function authMiddleware(req, res, next) {
|
||||
// DEV_MODE — полный обход (разработка)
|
||||
if (process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production') {
|
||||
req.user = {
|
||||
email: 'dev@test.local',
|
||||
clientId: 'WZ01325',
|
||||
companyId: '00000000-0000-0000-0000-000000000000',
|
||||
companyName: 'DEV',
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
// Извлекаем JWT из cookie или заголовка
|
||||
const token =
|
||||
req.cookies?.jwt ||
|
||||
(req.headers.authorization || '').replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
// Если нет токена — редиректим на логин (только GET, не API)
|
||||
if (req.method === 'GET' && !req.path.startsWith('/.well-known')) {
|
||||
return res.redirect(`/login?returnTo=${encodeURIComponent(req.originalUrl)}`);
|
||||
}
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verifyJWT(token);
|
||||
req.user = {
|
||||
email: payload.email || 'unknown',
|
||||
clientId: payload.ClientID,
|
||||
companyId: payload.company_id,
|
||||
companyName: payload.company_name || payload.ClientID,
|
||||
};
|
||||
next();
|
||||
} catch (e) {
|
||||
// Токен невалиден → чистим cookie и на логин
|
||||
res.clearCookie('jwt');
|
||||
if (req.method === 'GET' && !req.path.startsWith('/.well-known')) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── JWT-верификация ──
|
||||
|
||||
async function verifyJWT(token) {
|
||||
if (JWKS_URL) {
|
||||
// Продакшен: JWKS от auth-api
|
||||
const { payload } = await jose.jwtVerify(token, jose.createRemoteJWKSet(new URL(JWKS_URL)), {
|
||||
issuer: ISSUER,
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
// Mock: локальный публичный ключ
|
||||
const { payload } = await jose.jwtVerify(token, keyPair.publicKey, {
|
||||
issuer: ISSUER,
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
// ── JWT-выпуск (только mock) ──
|
||||
|
||||
async function issueJWT(claims) {
|
||||
if (!keyPair) throw new Error('JWKS_URL задан — выпуск токенов невозможен');
|
||||
|
||||
const privateKey = keyPair.privateKey;
|
||||
// Для jose.SignJWT нужен ключ в формате, который он понимает
|
||||
// Экспортируем как JWK и импортируем обратно как KeyLike
|
||||
const jwk = await crypto.subtle.exportKey('jwk', privateKey);
|
||||
|
||||
return new jose.SignJWT({
|
||||
ClientID: claims.clientId,
|
||||
company_id: claims.companyId,
|
||||
company_name: claims.companyName,
|
||||
email: claims.email,
|
||||
login: claims.email,
|
||||
})
|
||||
.setProtectedHeader({ alg: 'RS256', kid: 'mock-1' })
|
||||
.setIssuedAt()
|
||||
.setIssuer(ISSUER)
|
||||
.setExpirationTime('24h')
|
||||
.setSubject(claims.companyId)
|
||||
.sign(await jose.importJWK(jwk, 'RS256'));
|
||||
}
|
||||
|
||||
module.exports = { initAuth };
|
||||
Reference in New Issue
Block a user