Amazon S3 による Web ページの作成 (CloudFront/Cognito)2023年2月26日 | |
はじめにS3 バケットはパブリックアクセスが推奨されていない。S3 バケットの内容にパブリックアクセスを無効にした状態でアクセスするには、CloudFront を用いる。CloudFront を用いると、アクセスに対する柔軟な処理を追加したり、WAF を導入したり、キャッシュを用いたり (CDN サービスだからこれが本来の利用方法だが) できる。 S3 と CloudFront で静的 Web サイトを公開するディストリビューションの作成公開したい S3 バケットを作成しておく。 CloudFront: ディストリビューションの作成
CloudFront: S3 バケットの設定
S3
バケットポリシーは次のようになるかもしれない。
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::<バケット名>/index.html",
"arn:aws:s3:::<バケット名>/default.css",
"arn:aws:s3:::<バケット名>/*.js",
"arn:aws:s3:::<バケット名>/libs/*",
"arn:aws:s3:::<バケット名>/fig/*"
],
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<アカウント>:distribution/<ディストリビューション>"
}
}
}
]
}
確認: https://(ディストリビューションドメイン名)/index.html などでアクセスできるか確認する。 ※ディストリビューションのデフォルトルートオブジェクトやオリジンのオリジンパスで URL のアクセス方法を変えられる。 キャッシュキャッシュの確認Firefox の場合、開発ウインドウ (F12) の [ネットワーク] で項目を選んだときに、"x-cache" の値が "Hit from cloudfront" になっている場合は、CloudFront のキャッシュが効いている。 キャッシュ削除
キャッシュは 24 時間保持するらしいが、やりすぎるとお金がかかるらしい。 参考 キャッシュ無効化CloudFront の [ビヘイビア] の編集で、"キャッシュポリシー" に "CachingDisabled" を選ぶ。 関数CloudFront で使える関数として、Lambda@Edge と CloudFront Functions がある。Lambda@Edge は 5 s、CloudFront Functions は 1 ms で処理する必要がある。CloudFront Functions のほうが費用は安い。ただし、CloudFront Functions でサポートするのは ECMAScript 5.1 相当らしい。Lambda@Edge はリージョンをバージニア北部 (us-east-1) にする必要がある。 参考 Lambda@Edge
CloudFront イベント
お試し関数
export const handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
console.log(JSON.stringify(request, null, 2));
callback(null, request);
};
ビューワーリクエストに設定する。 CloudWatch のログは、アクセスされたリージョンに残るようである。 関数を更新する場合は、[Lambda@Edge へのデプロイ] で "この関数で既存の CloudFront トリガーを使用" を選べばよい。 ただ転送するだけの場合は、次のようにする。
export const handler = (event, context, callback) => {
const response = {
status: "302",
statusDescription: "Found",
headers: {
location: [{
key: "Location",
value: <URL>
}]
}
};
callback(null, response);
};
Cognito 認証S3 バケットの内容を直接公開する場合、Cognito 認証を用いても、コードを実行するためにある程度内容を公開する必要がある。CloudFront を用いると、CloudFront 側で Cognito 認証を行うことにより、認証を通らない限り S3 バケットの内容を少しも公開する必要がなくなる。 Cognito の認証にはいくつかのタイプがあり、認証の URL に "#token_id=..." でトークンを付与してくれるもの (暗黙的な付与) と、"?code=..." でトークン交換用のコードを付与してくるもの (認証コード付与) がある。前者は CloudFront で処理する方法がわからなかったし、そもそもトークンがモロ見えなのもアレなので、ここではコードを使う方法を用いる。 Cognito のアプリケーションクライアントを「秘密クライアント」にする (あとから切り替えられないので注意)。これでクライアントシークレットを用いることができる。"OAuth 2.0 許可タイプ" で "認証コード付与" を選ぶ。URL に code が付与されるので、それを使ってトークンエンドポイントからトークンをもらう必要がある。 Lambda@Edge の例Lambda@Edge で Cognito 認証するコードの例を示す。流れとしては以下のようになっている。
トークンの妥当性はもう少しちゃんと確認したほうがよさそうだが、どうせ S3 側で弾かれるので今回はこうしている。 S3 の index.html 側では、cookie から id_token を取り出して使う。id_token がなければ login に飛ばす。トークンによる認証がうまくいかなければ logout に飛ばす。logout では reset に飛ばしているが、cookie のトークンを空にするためである。
import https from "https";
import querystring from "querystring";
const cloudFrontName = <CloudFront 名>
const cognitoDomainName = <Cognito ドメイン名>
const cognitoAppClientId = <Cognito アプリクライアント ID>
const cognitoAppClientSecret = <Cognito アプリクライアントシークレット>
const cloudFrontUrl = `https://${cloudFrontName}.cloudfront.net`;
const indexUrl = `${cloudFrontUrl}/index.html`;
const resetUrl = `${cloudFrontUrl}/reset`;
const cognitoDomainUrl
= `https://${cognitoDomainName}.auth.ap-northeast-1.amazoncognito.com`;
const cognitoTokenUrl = `${cognitoDomainUrl}/oauth2/token`;
const cognitoLoginUrl
= `${cognitoDomainUrl}/login?response_type=code`
+ `&client_id=${cognitoAppClientId}&redirect_uri=${indexUrl}`;
const cognitoLogoutUrl
= `${cognitoDomainUrl}/logout`
+ `?client_id=${cognitoAppClientId}&logout_uri=${indexUrl}`;
function goLogin(callback) {
console.log("goLogin");
const response = {
status: "302",
statusDescription: "Found",
headers: {
location: [{
key: "location",
value: cognitoLoginUrl
}]
}
};
callback(null, response);
}
function goLogout(callback) {
console.log("goLogout");
const response = {
status: "302",
statusDescription: "Found",
headers: {
location: [{
key: "location",
value: cognitoLogoutUrl
}]
}
};
callback(null, response);
}
function convertCookieToJSON(cookie) {
if (cookie.trim() === "") {
return {};
}
const cookies = cookie.split("; ");
let json = {};
for (const s of cookies) {
const item = s.split("=");
json[item[0]] = item[1];
}
return json;
}
function goReset(request, callback) {
console.log("goReset");
const response = {
status: "302",
statusDescription: "Found",
headers: {
location: [{
key: "location",
value: resetUrl
}],
"set-cookie": [{
key: "set-cookie",
value: "id_token="
}]
}
};
callback(null, response);
}
function validIDToken(idToken) {
if (idToken === "") return ;
const tokens = idToken.split(".");
const obj = JSON.parse(Buffer.from(tokens[1], "base64").toString());
console.log(JSON.stringify(obj, null, 2));
if (obj.aud !== cognitoAppClientId) {
return false;
}
return true;
}
function checkIDToken(idToken, request, callback) {
if (validIDToken(idToken)) {
console.log("ok");
let date = new Date(Date.now());
date.setMonth(date.getMonth() + 1);
date = date.toUTCString();
const response = {
status: "302",
statusDescription: "Found",
headers: {
"location": [{
key: "location",
value: indexUrl
}],
"set-cookie": [{
"key": "set-cookie",
"value": `id_token=${idToken}; samesite=lax; expires=${date}`
}]
}
};
callback(null, response);
} else {
goLogout(callback);
}
}
function checkCode(code, request, callback) {
const basic = "Basic "
+ Buffer.from(cognitoAppClientId + ':' + cognitoAppClientSecret)
.toString("base64");
const options = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": basic
}
};
console.log(options);
const req = https.request(cognitoTokenUrl, options, (res) => {
console.log("statusCode:", res.statusCode);
console.log("headers:", res.headers);
res.on("data", (d) => {
console.log(d.toString());
const idToken = JSON.parse(d.toString()).id_token;
if (idToken === undefined) {
goLogout(callback);
} else {
checkIDToken(idToken, request, callback);
}
});
});
req.on("error", (e) => {
console.error(e);
});
const params = {
grant_type: "authorization_code",
client_id: cognitoAppClientId,
redirect_uri: indexUrl,
code: code
};
console.log(params);
const data = new URLSearchParams();
for(const name in params) {
data.append(name, params[name]);
}
console.log(data.toString());
req.write(data.toString());
req.end();
}
function forbidden(callback) {
console.log("forbidden");
const response = {
status: "403",
statusDescription: "Forbidden",
headers: {
"content-type": [{
key: "Content-Type",
value: "text/html"
}]
},
body: ""
};
callback(null, response);
}
function getQueryString(request, param) {
if (request.querystring !== undefined && request.querystring !== "") {
return querystring.parse(request.querystring)[param];
}
return undefined;
}
function getCode(request) {
return getQueryString(request, "code");
}
function getCookie(cookie, label) {
const json = convertCookieToJSON(cookie);
return json[label];
}
function getIDToken(request) {
const cookie = request.headers.cookie;
if (cookie !== undefined && cookie[0].value !== "") {
return getCookie(cookie[0].value, "id_token");
}
return undefined;
}
export const handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
console.log(JSON.stringify(event, null, 2));
/* index.html */
if (request.uri === "/index.html") {
const code = getCode(request);
const idToken = getIDToken(request);
console.log("code", code);
console.log("idToken", idToken);
/* code? */
if (code !== undefined) {
checkCode(code, request, callback);
/* ID token? */
} else if (idToken !== undefined && validIDToken(idToken)) {
callback(null, request);
} else {
goLogin(callback);
}
/* login */
} else if (request.uri === "/login") {
goLogin(callback);
/* logout */
} else if (request.uri === "logout") {
if (request.headers.cookie !== undefined) {
goReset(request, callback);
} else {
goLogout(callback);
}
/* reset */
} else if (request.uri === "/reset") {
goLogout(callback);
/* others */
} else {
if (request.headers.referer === undefined) {
forbidden(callback);
} else {
callback(null, request);
}
}
};
| |
| PENGUINITIS | |