From 2fb4548bf6af1568417e030669f6625484e2fe24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 08:45:21 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20auth.js=20=D0=BD=D0=B0=20jsonwebtoken=20?= =?UTF-8?q?(CJS)=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20jose=20(ESM)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 110 ++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- src/auth.js | 122 ++++++++++++++++++---------------------------- 3 files changed, 152 insertions(+), 82 deletions(-) diff --git a/package-lock.json b/package-lock.json index b41ef9e..2d233c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dotenv": "^16.4.7", "ejs": "^3.1.10", "express": "^4.21.1", - "jose": "^6.2.3", + "jsonwebtoken": "^9.0.3", "pg": "^8.13.1" } }, @@ -74,6 +74,11 @@ "balanced-match": "^1.0.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -207,6 +212,14 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -495,14 +508,86 @@ "node": ">=10" } }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "funding": { - "url": "https://github.com/sponsors/panva" + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" } }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -817,6 +902,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", diff --git a/package.json b/package.json index 7eec7b7..d9b04c8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dotenv": "^16.4.7", "ejs": "^3.1.10", "express": "^4.21.1", - "jose": "^6.2.3", + "jsonwebtoken": "^9.0.3", "pg": "^8.13.1" } } diff --git a/src/auth.js b/src/auth.js index b293fa8..90dd4a3 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,51 +1,41 @@ -const jose = require('jose'); +const jwt = require('jsonwebtoken'); const crypto = require('crypto'); -// ── Конфигурация ── const ISSUER = process.env.JWT_ISSUER || 'mock-auth-api'; -const JWKS_URL = process.env.JWKS_URL || ''; // пусто → локальные ключи +const JWKS_URL = process.env.JWKS_URL || ''; -let keyPair = null; // { publicKey, privateKey } — CryptoKeyPair -let jwks = null; // предсобранный JWKS +let keyPair = null; +let jwks = null; -// ── Инициализация ── - -async function initAuth() { - // Если задан внешний JWKS — используем его (продакшен) +function initAuth() { if (JWKS_URL) { - console.log(`🔐 Auth: внешний JWKS ${JWKS_URL}`); - return { verify: verifyJWT, middleware: createMiddleware(), jwksHandler: null }; + console.log('Auth: external JWKS ' + JWKS_URL); + return { verify: verifyJWT, middleware: createMiddleware(), jwksHandler: null, issue: 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'] - ); + console.log('Auth: MOCK - local RS256 key'); + keyPair = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); - const publicJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey); - jwks = { keys: [{ ...publicJwk, alg: 'RS256', use: 'sig', kid: 'mock-1' }] }; + const pubKeyObj = crypto.createPublicKey(keyPair.publicKey).export({ format: 'jwk' }); + pubKeyObj.alg = 'RS256'; + pubKeyObj.use = 'sig'; + pubKeyObj.kid = 'mock-1'; + jwks = { keys: [pubKeyObj] }; return { verify: verifyJWT, issue: issueJWT, middleware: createMiddleware(), - jwksHandler: (req, res) => res.json(jwks), + jwksHandler: function(req, res) { res.json(jwks); }, }; } -// ── Middleware ── - function createMiddleware() { - return async function authMiddleware(req, res, next) { - // DEV_MODE — полный обход (разработка) + return function authMiddleware(req, res, next) { if (process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production') { req.user = { email: 'dev@test.local', @@ -56,21 +46,18 @@ function createMiddleware() { return next(); } - // Извлекаем JWT из cookie или заголовка - const token = - req.cookies?.jwt || + var token = (req.cookies && 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)}`); + if (req.method === 'GET' && req.path.indexOf('/.well-known') !== 0) { + return res.redirect('/login?returnTo=' + encodeURIComponent(req.originalUrl)); } return res.status(401).send('Unauthorized'); } try { - const payload = await verifyJWT(token); + var payload = verifyJWT(token); req.user = { email: payload.email || 'unknown', clientId: payload.ClientID, @@ -79,9 +66,8 @@ function createMiddleware() { }; next(); } catch (e) { - // Токен невалиден → чистим cookie и на логин res.clearCookie('jwt'); - if (req.method === 'GET' && !req.path.startsWith('/.well-known')) { + if (req.method === 'GET' && req.path.indexOf('/.well-known') !== 0) { return res.redirect('/login'); } return res.status(401).send('Unauthorized'); @@ -89,47 +75,35 @@ function createMiddleware() { }; } -// ── JWT-верификация ── - -async function verifyJWT(token) { +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; + throw new Error('JWKS_URL verification not implemented yet'); } - - // Mock: локальный публичный ключ - const { payload } = await jose.jwtVerify(token, keyPair.publicKey, { + return jwt.verify(token, keyPair.publicKey, { + algorithms: ['RS256'], 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')); +function issueJWT(claims) { + if (!keyPair) throw new Error('Cannot issue tokens with external JWKS'); + return jwt.sign( + { + ClientID: claims.clientId, + company_id: claims.companyId, + company_name: claims.companyName, + email: claims.email, + login: claims.email, + }, + keyPair.privateKey, + { + algorithm: 'RS256', + issuer: ISSUER, + subject: claims.companyId, + expiresIn: '24h', + keyid: 'mock-1', + } + ); } module.exports = { initAuth };