This commit is contained in:
1 parent
8ed829cb72
commit
5091da737d
4 files changed
+68
-16
No files matched your search
+62
-11
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user