./knowledge-base/lambda/edge-auth/index.js
/**
* Lambda@Edge: /era-assist/* 認証ガード
*
* CloudFront viewer-request イベントで実行される。
* ブラウザが持つ okta_access_token Cookie の JWT 有効期限を確認し、
* - 有効: そのまま App Runner へ転送
* - 無効/なし: `/?redirect_to=/era-assist/` へリダイレクト
* → React SPA が Okta 認証後、元の /era-assist/ に戻す
*
* セキュリティ上の注意:
* - JWT の署名検証は行わず、有効期限(exp)のみチェックする軽量実装
* - App Runner への直接アクセスは WAF (X-Origin-Secret ヘッダー) で別途防御済み
* - このガードはUXのため(未認証ユーザーを Okta へ誘導する目的)
*/
"use strict";
/**
* Cookie ヘッダーを解析して Map に変換する
* @param {string} cookieHeader - "name=value; name2=value2" 形式の文字列
* @returns {Record<string, string>}
*/
function parseCookies(cookieHeader) {
if (!cookieHeader) return {};
return Object.fromEntries(
cookieHeader.split(";").map((c) => {
const eq = c.indexOf("=");
if (eq === -1) return [c.trim(), ""];
return [c.slice(0, eq).trim(), c.slice(eq + 1).trim()];
}),
);
}
/**
* JWT の payload を base64url デコードして expiry を確認する
* @param {string} token - JWT 文字列
* @returns {boolean} 有効期限内なら true
*/
function isTokenValid(token) {
try {
const parts = token.split(".");
if (parts.length !== 3) return false;
// base64url → base64 変換してデコード
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - (base64.length % 4)) % 4);
const payload = JSON.parse(
Buffer.from(base64 + padding, "base64").toString("utf8"),
);
// exp クレームが存在し、現在時刻より後であることを確認
if (!payload.exp) return false;
return payload.exp > Math.floor(Date.now() / 1000);
} catch {
return false;
}
}
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// Cookie を取得・解析
const cookieHeader = headers.cookie?.[0]?.value || "";
const cookies = parseCookies(cookieHeader);
const token = cookies["okta_access_token"];
// トークンが有効なら App Runner へ転送(パスリライト付き)
if (token && isTokenValid(token)) {
// /era-assist/xxx → /xxx へリライト(CloudFront Function の代替)
const uri = request.uri;
if (uri.startsWith("/era-assist/")) {
request.uri = uri.slice(11); // "/era-assist" の11文字を除去
} else if (uri === "/era-assist") {
request.uri = "/";
}
return request;
}
// 未認証: React SPA のトップへリダイレクト
// redirect_to パラメータを付けることで、Okta 認証後に元の URL に戻る
const originalPath =
request.uri + (request.querystring ? "?" + request.querystring : "");
const redirectTo = encodeURIComponent(originalPath || "/era-assist/");
return {
status: "302",
statusDescription: "Found",
headers: {
location: [
{
key: "Location",
value: `/?redirect_to=${redirectTo}`,
},
],
"cache-control": [
{
key: "Cache-Control",
value: "no-cache, no-store",
},
],
},
};
};