はじめに
Okta + Lambda@Edge を使って、S3上の静的サイトにログイン制御をつける方法を解説します。さらに、OIDC の仕組みでよく出てくる nonce・state・PKCE の検証を実際にコードでどう実装するのかも見ていきます。
完成イメージ
- Oktaでログイン済みのユーザーのみあサイト(Next.js)を見ることができる
- 画像ファイルのURLに直接Curlしても、認証されていないとアクセスできない
※Lambda@EdgeはNode.jsで作成しています。
構成図
大まかな全体像はこんな感じです。
ユーザーが CloudFront 経由でサイトにアクセスすると、Lambda@Edge が Okta発行 の ID トークンを持っているかを確認します(②ログインチェック)。なければ Okta のログイン画面にリダイレクト。ログイン後に ID トークンを Cookie に保存し、元のページを表示する仕組みです。
図で見ると以下のようになります👇
この段階では雰囲気だけつかんでもらえればOKです!
動画版ハンズオン
動画版ハンズオンも作成しています!
「とりあえず作業を眺めたい」「動画で確認しながら手を動かしたい」と言う方は是非見てみて下さい![]()
前提
- Node.js(私の環境ではv22.14.0)
- AWSのアカウント
- Oktaのアカウント
Oktaのアカウントが無い人は下記で登録して下さい。この記事では「Access the Okta
Integrator Free Plan」を使っています。
手順
30分 ~ 1時間ぐらいの内容として想定しています。
- S3作成・サイトのアップロード
- CloudFront作成
- Okta管理画面でアプリケーション作成
- Okta管理画面で認可サーバー追加
- Lambda@Edgeのロジック・OIDCの認証フローの解説
- Lambda@Edge作成
- 想定通り動くかサイトを検証
1. S3作成・サイトのアップロード
サイトのコードを作成
GithubからコードをDL
下記よりDLしてください(「Code」→ 「Download Zip」)
(ブランチ:main)
サイトをビルドする
ライブラリをインストール
npm install
サイトの確認
特に何も変更しないで大丈夫です。
npm run dev
http://localhost:3000/で下記が表示されます。
サイトのビルド
ちゃんとサイトが表示されているのを確認したらビルドします。
npm run build
outディレクトリが作成されれば成功です。
out
├── _next
│ ├── 省略
│ └── static
│ └── 省略
├── 404
│ └── index.html
├── 404.html
├── favicon.png
├── index.html
├── index.txt
└── logo.png
S3バケットを作成してアップロードして行きます。
S3バケット作成
上記でbuildしたコードをアップロードするS3バケットを作成します。
- バケット名
- employee-site-jiroyoyogi(※ご自身の名前などに変更)
その他、デフォルトでOKです!ブロックパブリックアクセス設定は「パブリックアクセスをすべてブロック」にチェックしたままでお願いします。
サイトをアップロードする
先ほどbuildしたout/の中身を全てアップロードします。
✅ ここまででS3作成・サイトアップロードの作業は完了です!
2. CloudFront作成
ディストリビューションを作成
CloudFrontに移動して「ディストリビューションを作成」します。
Get started
- Distribution name
- employee-site
その他そのままで「Next」します。
Specify origin
- Origin type
- Amazon S3
- Origin
- 先ほど作成したバケットを選択する
- 例)employee-site-jiroyoyogi.s3.ap-northeast-1.amazonaws.com
その他そのままで「Next」します。
Enable security
- Web Application Firewall(WAF)
- セキュリティ保護を有効にしないでください
「Next」します。
Review and create
「Create distribution」します。
↓
キャッシュを無効にする
今回の構成ならば不要かとは思いますが認証系のコンテンツはキャッシュさせないのが基本かなと思うので、コンテンツがキャッシュされないように変更します。
「ビヘイビア」タブで下記のように選択して「編集」します。
「キャッシュキーとオリジンリクエスト」の「キャッシュポリシー」を「CachingDisabled」に変更します。変更したら「Save changes」します。
サイトの確認
「一般」タブで「最終変更日」が更新されたのを確認します。
「ディストリビューションドメイン名」をコピーしてブラウザで表示確認します。
AccessDenideと表示されたらドメインに/index.htmlを付けて再度アクセスして下さい。
例)https://abcdefg123.cloudfront.net/index.html
CloudFrontではデフォルトのインデックスドキュメントが設定されていないため、明示的に index.html を指定する必要があります(後ほどLambda@Edgeで補います)。
ローカルホストで表示してたサイトが現れたら成功です。
✅ ここまででAWS・マネコンでの設定は完了です!
3. Okta管理画面でアプリケーション作成
下図のアプリケーション画面にて「アプリ統合」します。
- サインイン方法
- OIDC -OpenID Connect
- アプリケーションタイプ
- Webアプリケーション
上記のように設定して「次へ」移動します。
- アプリ統合名
employee site edge
- サインインリダイレクトURI
例)https://12345abcd.cloudfront.net/callback
※CFのドメインはご自身のもので置き換えて下さい。
- サインアウトリダイレクトURI
例)https://12345abcd.cloudfront.net/
末尾「/」ありでお願いします。無くてもいけると思いますが未検証です。
- その他
そのままでOKです。
- アクセス制御
- Organizationの全員にアクセスを許可
扱い方を理解してる方は「選択されたグループにアクセスを制御」でも大丈夫です。
「保存」しましょう。
追加の検証としてPKCEを要求
「編集」してProof Key for Code Exchangeの項目にて「追加の検証としてPKCEを要求」にチェックを入れます。チェックを入れたら「保存」します。
✅ Oktaのアプリケーション作成が出来ました!次に認可サーバーを作成して行きます!
4. Okta管理画面で認可サーバー追加
左ペイン「セキュリティ」→「API」を選択して画面移動します。「認可サーバーを追加」
- 名前
- employee-site
- オーディエンス
- api://employee-site
「保存」します。
「アクセスポリシー」タブに移動し「ポリシーを追加」します。
- 名前
- employee-site
- 説明
- employee-site
- 次に割り当てる:
- 「employee site edge」を選択(※)
(※)入力欄に「e」と入力すると選択肢が表示されます
先ほど作成したアプリケーションと認可サーバーとの紐付けをしています。
「ポリシーを作成」します。
「ルールを追加」します。ユーザーに発行されるアクセストークンの有効期限などのルール設定をします。
- ルール名
- authenticated rule
デフォルトのままで「ルールを作成」します。
✅ お疲れ様です!Oktaでの設定は以上になります!!
5. Lambda@Edgeのロジック・OIDCの認証フローの解説
GithubからコードをDL
下記よりDLしてください(「Code」→ 「Download Zip」)
(ブランチ:main)
complete.mjs → index.mjs
DL出来たら以下の作業をお願いします🙇
- index.mjsを削除
- complete.mjsをindex.mjsにリネーム
※index.mjsは動画ハンズオンのためのものです。
ポイントを解説
index.mjsの大枠
- CloudFrontへのリクエストをhandlerがまず受け止める
- URLにindex.htmlがなかったら追加する(さっき手動でつけたヤツ)
- cookieからIDトークンを取り出す
- IDトークンが存在するか?IDトークンが有効なものか?検証
- 検証NG → Oktaにログインしに行く(redirectToLogin関数)
- 検証OK → S3にオリジンアクセス(index.htmlなどファイル取得)
大枠部分のコードのみ抜粋。全体はDLしたもので確認して下さい。
// CloudFrontへのリクエストをhandlerがまず受け止める
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// cookieからIDトークンを取り出す
const cookies = cookie.parse(headers.cookie?.[0]?.value || "");
// ディレクトリインデックス
// URLにindex.htmlがなかったら追加する(さっき手動でつけたヤツ)
if (request.uri.endsWith("/")) {
request.uri += "index.html";
} else if (!request.uri.includes(".")) {
request.uri += "/index.html";
}
const idToken = cookies["ID_TOKEN"];
// IDトークンが存在するか?
if (!idToken) {
// 検証NG → Oktaにログインしに行く
return redirectToLogin(request.uri);
}
// IDトークンが有効なものか?
try {
jwt.verify(idToken, pem, {
algorithms: ["RS256"],
issuer: OKTA_ISSUER,
audience: CLIENT_ID,
});
} catch (err) {
console.log("ID Token verification error:", err);
// 検証NG → Oktaにログインしに行く
return redirectToLogin(request.uri);
}
return request;
}
none・state・PKCE
OIDCの認証フロー。なりすましやトークンの乗っ取りなどを防ぐためのもの。
- state ... CSRF対策
- nonce ... リプレイ攻撃防止
- PKCE ... 認可コード奪取防止
用途は違うものの、いずれもランダムな文字列。以下のように作成。
// state ランダムな文字列
const stateToken = crypto.randomBytes(16).toString("hex");
// nonce ランダムな文字列
const nonce = crypto.randomBytes(16).toString("hex");
// PKCE
// 1. ランダムな秘密の文字列(クライアント側に保存)
const codeVerifier = crypto.randomBytes(32).toString("base64url");
// 2. SHA-256でハッシュ値(バイナリ)作成
const codeVerifierHash = crypto.createHash("sha256").update(codeVerifier).digest();
// 3. ハッシュ値をbase64urlに変換。URLで使える文字列に変換
const codeChallenge = Buffer.from(codeVerifierHash).toString("base64url")
ユーザーがOktaの画面でログインするなど認証の段階が進む中で、乗っ取りなどされていないかの検証をする際に上記の文字列たちを使います。
検証のイメージ(※)は次のような感じ。
各文字列を最初に作り、認証の段階が進む中で伝言ゲーム(バケツリレー)する。最初と最後が同じならば、途中でおかしなことは起きていない。
念押しますがあくまでイメージです。
PKCEは文字列が2つ必要
上記コードでPKCEのみ2つ文字列を作成しています。
- ランダムな文字列
- ↑をハッシュ値に変えたもの
何故かというとPKCE認証のフローが以下のようになるからです。
【PKCE認証のフロー】
ランダムな文字列とハッシュ値を作成
↓
ランダムな文字列を手元に保存
↓
ハッシュ値をOktaに送る。Oktaが保存
↓
ユーザーログイン
↓
Oktaが認可コード発行
↓
受け取った認可コードとランダムな文字列をOktaに送る
↓
Oktaでランダムな文字列を元に保存してたハッシュ値を再現出来るか検証(PKCE認証)
↓
IDトークン・アクセストークン発行
cookieに最初作った文字列達を保存する
各文字列を最初に作り、認証の段階が進む中で伝言ゲーム(バケツリレー)する。最初と最後が同じならば、途中でおかしなことは起きていない。
「最初と最後が同じならば」と書きました。最初の状態はブラウザのcookieに保存します。どのように保存するか?Lambda@EdgeからOktaに以下のようにリダイレクトさせる直前でset-cookieして保存してます。
return {
status: "302",
headers: {
location: [{ key: "Location", value: authUrl }],
// nonce検証するためにブラウザのcookieに保存
"set-cookie": [
{
key: "Set-Cookie",
value: cookie.serialize("NONCE", nonce, {
path: "/",
httpOnly: true,
}),
},
// PKCE検証するためにブラウザのcookieに保存
{
key: "Set-Cookie",
value: cookie.serialize("PKCE", codeVerifier, {
path: "/",
httpOnly: true,
}),
},
// state検証するためにブラウザのcookieに保存
{
key: "Set-Cookie",
value: cookie.serialize("STATE", stateToken, {
path: "/",
httpOnly: true,
}),
},
],
},
};
}
Lambdaで認証されるまでのイメージ
🔑 本図で使っている「鍵」アイコンは、state、nonce、code_verifier のような「検証用のランダム値」を視覚的に表そうとトライしたものです。暗号学的な「鍵(key)」とは別物です。温かい目で見ていただけたら幸いです。
ログインコールバックURLへ飛ばせ!
OktaでログインするとOktaの管理画面で指定した「サインインリダイレクトURI」にリダイレクトさせられます。下記のようにクエリに認可コードとstateがくっ付いて来ます。
https://abcd123.cloudfront.net/callback?code=xxx&state=yyy
リダイレクトするとどうなるか?
Lambda@Edgeで「/callback」に来た場合の処理を受け止めます。
下記コードは先ほどの"大枠部分のコード"の冒頭に/callbackの処理を追加したものです。クエリに付いていた認証コードやstateであったり、cookieの中の各種値を取り出して、検証を行っていきます。
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const cookies = cookie.parse(headers.cookie?.[0]?.value || "");
// Hosted UIからの戻って来たリクエスト
if (request.uri.startsWith("/callback")) {
// Hosted UIでログインするとコールバックURLに色々なクエリが追加される
const query = new URLSearchParams(request.querystring);
// Hosted UI(Okta)が発行した一時的な短命のチケット
// トークンと引き換えれる。引き換え時にPKCE検証が行われる
const code = query.get("code");
// 認可リクエスト時にくっつけたクエリが返ってくる。改竄されていないか後でチェック
const state = query.get("state");
// クッキーに保存してた nonce を取得
const nonce = cookies["NONCE"];
// クッキーに保存してた codeVerifier(秘密の文字列)を取得
const codeVerifier = cookies["PKCE"];
// クッキーに保存してた state を取得
const stateTokenFromCookie = cookies["STATE"];
~ 以下略 ~
}
}
✅ ロジックのポイントは以上です。次で実際のコードを完成・アップロードして行きます。
6. Lambda@Edge作成
complete.mjs → index.mjs
説明を挟んでしまったので再喝します。こちらよりコードをDLして下さい。DL出来たら以下の作業をお願いします🙇
- index.mjsを削除
- complete.mjsをindex.mjsにリネーム
※index.mjsは動画ハンズオンのためのものです。
変数をセットする
コード上部にある4つの変数に値を埋めます。
const OKTA_ISSUER = "";
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const CLOUD_FRONT_DOMAIN = "";
OKTA_ISSUER
4.で追加した認可サーバーの発行者URLをセットします。
CLIENT_ID
3.で作成したアプリケーションのクライアントIDをセットします。
CLIENT_SECRET
↑の画面を少しスクロールすると「クライアントシークレット」の項目があります。クリップボードをクリックしてコピーしてセットします。
CLOUD_FRONT_DOMAIN
2.で作成したCloudFrontの「ディストリビューションドメイン名」をコピーしてセットします。
セット後のイメージ
const OKTA_ISSUER = "https://trial-3722188.okta.com/oauth2/ausskbdm68iahhYpx697";
const CLIENT_ID = "0oaskbaxofqQQKeXW697";
const CLIENT_SECRET = "1vb8Am1JMu2jHBlCI9oesDkX-TTJcZrHSeOpltUMDcpQEbLq9uds9OTr8_rxJ5cS";
const CLOUD_FRONT_DOMAIN = "https://d3kdten9gkheqe.cloudfront.net";
CLOUD_FRONT_DOMAINの最後は「/」無しでお願いします。
NG: https://d3kdten9gkheqe.cloudfront.net/
OK: https://d3kdten9gkheqe.cloudfront.net
CLIENT_SECRETは記事公開時点で削除済みのものをセットしてます。
アップロード準備
モジュールのインストール
index.mjsやpackage.jsonのあるディレクトリにて下記コマンド実行
npm install
コードをzipする
アップロードするファイル達をzipします。
index.mjsやpackage.jsonのあるディレクトリにて下記コマンド実行
zip -r function.zip index.mjs package.json node_modules
以下のようにfunction.zipが出来たらOKです!
.
├── function.zip
├── index.mjs
├── node_modules/~省略~
├── oidc.png
├── package-lock.json
├── package.json
└── README.md
アップロード
バージニア北部でLambda作成
AWSのマネコンでLambdaに移動します。リージョンを「バージニア北部」に切り替えます。
※Lambda@Edgeはバージニア北部でしか作れません。
関数の作成
- 関数名
- employee-site-lambda-edge
- ランタイム
- Node.js22.x
- アーキテクチャ
- x86_64
デフォルトの実行ロールの変更
- 実行ロール
- AWS ポリシーテンプレートから新しいロールを作成
- ロール名
- employee-site-lambda-edge-role
下図のように「基本的なLambda@Edgeのアクセス権限(CloudFrontトリガーの場合)」を選択
上記のように設定した上で「関数の作成」をします。
zipアップロード
下図の右下「.zipファイル」を選択
先ほど作成したfunction.zipをアップロードします。
コードソースが更新されました。
タイムアウト時間を延長する
「設定」タブ→「一般設定」→「編集」します。
タイムアウトを 5秒 に設定します。これはビューワーリクエストにつけるLambda@Edgeの最大タイムアウト時間です。
これをしないとコールドスタートの場合にタイムアウトになる可能性が増えます。ちなみに、タイムアウトの場合は503エラーの画面が表示されます。
Lambda@Edgeへのデプロイ
下図の右上の「アクション」→「Lambda@Edgeへのデプロイ」を選択します。
- オプションを選択
- 新しいCloudFrontトリガーの設定
- ディストリビューション
- 作成したCloudFrontを選択
- キャッシュ動作
- *
- CloudFrontイベント
- ビューアーリクエスト
設定出来たら「デプロイ」します。
CloudFrontへ移動
下図の右の「最終変更日」に日時が入るのを待ちます。
↓
✅ あとは確認するのみです!!
想定通り動くかサイトを検証
サイトにアクセス
「ディストリビューションドメイン名」をコピーしてアクセスするとOktaのログイン画面に飛ばされます。
ログインするとS3に置いたサイトが表示されました!
URLに/index.htmlがちゃんと追加されているのも確認出来ます。
cookieのIDトークンを確認
開発者ツールを開き「Application」タブ→「Cookies」を開きます。
base64でエンコードされたIDトークンが入ってることが確認出来ます。
トークンの中身を確認
最後にトークンの中身も確認しておきます。
「Cookie Value」をコピーした上でhttps://jwt.io/ にアクセス、「Encoded value」の入力欄に貼り付けます。
↓
トークンの中にメールアドレスなどが含まれてることが確認出来ます。
完成
この記事での実装は以上となります。
もし、さらにユーザー名も表示したいという方いらっしゃいましたら、下記動画でお伝えしてるので、是非ご活用下さい。51分28秒あたりが該当部です。
おわりに
これまで、OktaとCognitoを連携させてS3ホスティングのサイトにログイン認証を加える構成は何度か試してきました。ただ、「Cognitoを使わず、Okta単体で完結できないか?」と試してみたい気持ちがあり、今回の構成を試してみることにしました。
また、OIDCでよく出てくる state や nonce、PKCE といった用語について、これまでは何となく「必要そうなもの」として扱ってきましたが、実際に一から実装してみることで、それぞれの役割や検証方法について深く理解できたのが大きな収穫です。
Lambda@Edgeの扱いやOIDCフローなど、細かいところでハマることも多かったですが、それだけに得られる学びも多く、やって良かったと思える構成でした。
ここまで読んでいただきありがとうございましたー!


















































