fix: 피드백 작성 jWT 오류 개선
Deploy EG-BIM QA Gateway / deploy (push) Successful in 51s

This commit is contained in:
root committed 2026-10-23 15:14:35 +09:00
1 parent 8ed829cb72
commit 5091da737d
4 files changed
+68 -16

No files matched your search

+62 -11
View File
@@ -144,6 +144,28 @@ async function verifyValue(signed, secret) {
return difference === 0 ? value : null;
}
async function encryptValue(value, secret) {
const keyMaterial = await sha256(`baron-qa-session-encryption-v1:${secret}`);
const key = await crypto.subtle.importKey('raw', keyMaterial, { name: 'AES-GCM' }, false, ['encrypt']);
const iv = randomBytes(12);
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoder.encode(value));
return `v1.${base64url(iv)}.${base64url(new Uint8Array(ciphertext))}`;
}
async function decryptValue(encrypted, secret) {
if (!encrypted || !secret) return null;
const [version, encodedIv, encodedCiphertext] = encrypted.split('.');
if (version !== 'v1' || !encodedIv || !encodedCiphertext) return null;
try {
const keyMaterial = await sha256(`baron-qa-session-encryption-v1:${secret}`);
const key = await crypto.subtle.importKey('raw', keyMaterial, { name: 'AES-GCM' }, false, ['decrypt']);
const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: fromBase64url(encodedIv) }, key, fromBase64url(encodedCiphertext));
return decoder.decode(plaintext);
} catch (error) {
return null;
}
}
function randomBytes(size = 32) {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
@@ -297,6 +319,7 @@ async function finishLogin(request, env) {
const tokenResponse = await fetch(env.AUTH_TOKEN_URL, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: tokenParams });
const tokens = await tokenResponse.json().catch(() => ({}));
if (!tokenResponse.ok) return oauthError(env, 'oauth_token_exchange_failed', tokens.error_description || tokens.error || '', 502);
if (typeof tokens.access_token !== 'string' || !tokens.access_token) return oauthError(env, 'oauth_access_token_missing', '', 502);
let claims = parseJwt(tokens.id_token || tokens.access_token);
if (state.nonce && claims.nonce && state.nonce !== claims.nonce) return oauthError(env, 'invalid_oauth_nonce', '', 400);
if (env.AUTH_USERINFO_URL && tokens.access_token) {
@@ -306,19 +329,43 @@ async function finishLogin(request, env) {
}
const user = normalizeClaims(claims);
if (!user.ssoSubject) return oauthError(env, 'sso_subject_missing', '', 502);
const sessionPayload = base64url(JSON.stringify({ user, expiresAt: Date.now() + Number(env.SESSION_TTL_SECONDS || 3600) * 1000 }));
const session = await signValue(sessionPayload, env.SESSION_SECRET);
return redirect(new URL(state.returnUrl || '/', url.origin).toString(), { 'set-cookie': [cookie(env.SESSION_COOKIE_NAME, session, Number(env.SESSION_TTL_SECONDS || 3600)), cookie(env.OAUTH_COOKIE_NAME, '', 0, { delete: true })] });
const now = Date.now();
const accessToken = typeof tokens.access_token === 'string' ? tokens.access_token : '';
const accessTokenClaims = parseJwt(accessToken);
const tokenExpiresAt = Number(tokens.expires_in) > 0
? now + Number(tokens.expires_in) * 1000
: (Number(accessTokenClaims.exp) > 0 ? Number(accessTokenClaims.exp) * 1000 : now + Number(env.SESSION_TTL_SECONDS || 3600) * 1000);
const sessionExpiresAt = Math.min(now + Number(env.SESSION_TTL_SECONDS || 3600) * 1000, tokenExpiresAt);
if (sessionExpiresAt <= now) return oauthError(env, 'oauth_access_token_expired', '', 502);
const sessionPayload = JSON.stringify({
user,
accessToken,
accessTokenExpiresAt: Math.min(sessionExpiresAt, tokenExpiresAt),
expiresAt: sessionExpiresAt
});
const session = await encryptValue(sessionPayload, env.SESSION_SECRET);
const sessionMaxAge = Math.max(1, Math.floor((sessionExpiresAt - now) / 1000));
return redirect(new URL(state.returnUrl || '/', url.origin).toString(), { 'set-cookie': [cookie(env.SESSION_COOKIE_NAME, session, sessionMaxAge), cookie(env.OAUTH_COOKIE_NAME, '', 0, { delete: true })] });
}
async function getSessionContext(request, env) {
const signedSession = getCookie(request, env.SESSION_COOKIE_NAME);
const value = await decryptValue(signedSession, env.SESSION_SECRET);
if (!value) return null;
try {
const payload = JSON.parse(value);
if (!(payload.expiresAt > Date.now()) || !payload.user) return null;
return {
user: payload.user,
accessToken: typeof payload.accessToken === 'string' ? payload.accessToken : '',
accessTokenExpiresAt: Number(payload.accessTokenExpiresAt) || 0
};
} catch (error) { return null; }
}
async function getSession(request, env) {
const signedSession = getCookie(request, env.SESSION_COOKIE_NAME);
const value = await verifyValue(signedSession, env.SESSION_SECRET);
if (!value) return null;
try {
const payload = JSON.parse(decoder.decode(fromBase64url(value)));
return payload.expiresAt > Date.now() ? payload.user : null;
} catch (error) { return null; }
const session = await getSessionContext(request, env);
return session?.user || null;
}
async function sessionResponse(request, env) {
@@ -328,8 +375,11 @@ async function sessionResponse(request, env) {
async function createFeedback(request, env) {
if (request.method !== 'POST') return json({ error: 'method_not_allowed' }, 405, { allow: 'POST' });
const user = await getSession(request, env);
const session = await getSessionContext(request, env);
const user = session?.user;
if (!user) return json({ error: 'unauthenticated' }, 401);
if (!session.accessToken) return json({ error: 'sso_access_token_missing' }, 401);
if (session.accessTokenExpiresAt && session.accessTokenExpiresAt <= Date.now()) return json({ error: 'sso_access_token_expired' }, 401);
if (!env.ABC_API_KEY) return json({ error: 'feedback_api_not_configured' }, 503);
if (!env.ABC_PROJECT_ID || !env.ABC_CHANNEL_ID) return json({ error: 'feedback_target_not_configured' }, 503);
@@ -390,6 +440,7 @@ async function createFeedback(request, env) {
const idempotencyConsumer = env.ABC_IDEMPOTENCY_CONSUMER || sourceNamespace;
const upstreamHeaders = {
accept: 'application/json',
authorization: `Bearer ${session.accessToken}`,
'x-api-key': env.ABC_API_KEY,
'x-source-namespace': sourceNamespace,
'x-source-record-id': feedbackId,