From 21a2c65296a6162bc9778345a6197d2bb0e54753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 12 Jun 2026 16:31:18 +0400 Subject: [PATCH] =?UTF-8?q?v2:=20IAM=20fallback=20=E2=80=94=20JWT=20claims?= =?UTF-8?q?=20=D0=B5=D1=81=D0=BB=D0=B8=20IAM=20=D0=BD=D0=B5=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=83=D0=BF=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- v2/src/auth/index.js | 90 ++++++++++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 619dc31..a5b44a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.5.63", + "version": "0.5.64", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/v2/src/auth/index.js b/v2/src/auth/index.js index 4a6a5c7..8bf66c8 100644 --- a/v2/src/auth/index.js +++ b/v2/src/auth/index.js @@ -1,10 +1,8 @@ -// ═══════════════════════════════════════════════════════════════════════════════ // Модуль аутентификации V2 -// ═══════════════════════════════════════════════════════════════════════════════ - const crypto = require('crypto'); const https = require('https'); const http = require('http'); +const jwt = require('jsonwebtoken'); function buildAuthUrl(config) { const { baseUrl, clientId, redirectUri, state } = config; @@ -29,13 +27,8 @@ async function exchangeCode(code, config) { }); console.log('[v2:auth] exchangeCode →', tokenUrl); - console.log('[v2:auth] body:', body.toString()); - const result = await postForm(tokenUrl, body); console.log('[v2:auth] response keys:', Object.keys(result)); - if (!result.access_token) { - console.log('[v2:auth] NO ACCESS_TOKEN, full response:', JSON.stringify(result).slice(0, 500)); - } return result; } @@ -72,30 +65,62 @@ async function fetchIamUser(token, iamUrl) { }; } +function fallbackSessionUser(accessToken) { + const payload = jwt.decode(accessToken); + if (!payload) throw new Error('Failed to decode JWT'); + + const email = payload.email || payload.preferred_username || ''; + const name = payload.name || payload.preferred_username || email; + + return { + email: email, + clientId: payload.azp || payload.client_id || '', + allClientIds: [payload.azp || ''].filter(Boolean), + activeClientId: payload.azp || '', + activeProfileId: null, + companyId: '', + companyName: payload.azp || '', + isAdmin: false, + fio: name, + profiles: [], + isImpersonated: false, + impersonationType: null, + originalUserEmail: '', + originalUserFullName: '', + originalUserCompany: '', + }; +} + async function login(code, oidcConfig, iamUrl) { const tokenData = await exchangeCode(code, oidcConfig); const accessToken = tokenData.access_token; - if (!accessToken) throw new Error('No access_token in Keycloak response: ' + JSON.stringify(tokenData).slice(0, 300)); + if (!accessToken) throw new Error('No access_token in Keycloak response'); - const iamData = await fetchIamUser(accessToken, iamUrl); - - const sessionUser = { - email: iamData.email, - clientId: iamData.clientId, - allClientIds: iamData.allClientIds, - activeClientId: iamData.clientId, - activeProfileId: iamData.activeProfileId, - companyId: iamData.companyId, - companyName: iamData.companyName, - isAdmin: iamData.isAdmin, - fio: iamData.fio, - profiles: iamData.profiles, - isImpersonated: iamData.isImpersonated, - impersonationType: iamData.impersonationType, - originalUserEmail: iamData.originalUserEmail, - originalUserFullName: iamData.originalUserFullName, - originalUserCompany: iamData.originalUserCompany, - }; + let sessionUser; + try { + const iamData = await fetchIamUser(accessToken, iamUrl); + sessionUser = { + email: iamData.email, + clientId: iamData.clientId, + allClientIds: iamData.allClientIds, + activeClientId: iamData.clientId, + activeProfileId: iamData.activeProfileId, + companyId: iamData.companyId, + companyName: iamData.companyName, + isAdmin: iamData.isAdmin, + fio: iamData.fio, + profiles: iamData.profiles, + isImpersonated: iamData.isImpersonated, + impersonationType: iamData.impersonationType, + originalUserEmail: iamData.originalUserEmail, + originalUserFullName: iamData.originalUserFullName, + originalUserCompany: iamData.originalUserCompany, + }; + console.log('[v2:auth] IAM OK:', iamData.email, iamData.clientId); + } catch (iamErr) { + console.log('[v2:auth] IAM unavailable, using JWT fallback:', iamErr.message); + sessionUser = fallbackSessionUser(accessToken); + } return { sessionUser, token: accessToken, idToken: tokenData.id_token || null }; } @@ -109,9 +134,9 @@ function httpGet(url, token) { headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }, }, (res) => { let data = ''; - res.on('data', c => { data += c; if (data.length > 100_000) { req.destroy(); reject(new Error('Response too large')); } }); + res.on('data', c => { data += c; if (data.length > 100_000) { req.destroy(); reject(new Error('Too large')); } }); res.on('end', () => { - if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)); + if (res.statusCode !== 200) return reject(new Error('HTTP ' + res.statusCode + ': ' + data.slice(0, 200))); resolve(data); }); }); @@ -134,12 +159,11 @@ function postForm(urlStr, body) { let data = ''; res.on('data', c => data += c); res.on('end', () => { - console.log('[v2:auth] postForm status:', res.statusCode, 'body:', data.slice(0, 300)); - if (res.statusCode !== 200) return reject(new Error(`POST ${res.statusCode}: ${data.slice(0, 200)}`)); + if (res.statusCode !== 200) return reject(new Error('POST ' + res.statusCode + ': ' + data.slice(0, 200))); try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); - req.on('error', (e) => { console.log('[v2:auth] postForm ERROR:', e.message); reject(e); }); + req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); }); req.write(postData); req.end();