0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Okta × Lambda@Edge】CloudFront × S3 サイトにログイン機能を追加するハンズオン

Last updated at Posted at 2025-06-26

はじめに

Okta + Lambda@Edge を使って、S3上の静的サイトにログイン制御をつける方法を解説します。さらに、OIDC の仕組みでよく出てくる nonce・state・PKCE の検証を実際にコードでどう実装するのかも見ていきます。

完成イメージ

  • Oktaでログイン済みのユーザーのみあサイト(Next.js)を見ることができる
  • 画像ファイルのURLに直接Curlしても、認証されていないとアクセスできない

※Lambda@EdgeはNode.jsで作成しています。

成果物.png

構成図

大まかな全体像はこんな感じです。

ユーザーが CloudFront 経由でサイトにアクセスすると、Lambda@Edge が Okta発行 の ID トークンを持っているかを確認します(②ログインチェック)。なければ Okta のログイン画面にリダイレクト。ログイン後に ID トークンを Cookie に保存し、元のページを表示する仕組みです。

図で見ると以下のようになります👇

構成図2.png

この段階では雰囲気だけつかんでもらえればOKです!

:tv: 動画版ハンズオン

動画版ハンズオンも作成しています!

「とりあえず作業を眺めたい」「動画で確認しながら手を動かしたい」と言う方は是非見てみて下さい:pray:

前提

  • Node.js(私の環境ではv22.14.0)
  • AWSのアカウント
  • Oktaのアカウント

Oktaのアカウントが無い人は下記で登録して下さい。この記事では「Access the Okta
Integrator Free Plan」を使っています。

手順

30分 ~ 1時間ぐらいの内容として想定しています。

  1. S3作成・サイトのアップロード
  2. CloudFront作成
  3. Okta管理画面でアプリケーション作成
  4. Okta管理画面で認可サーバー追加
  5. Lambda@Edgeのロジック・OIDCの認証フローの解説
  6. Lambda@Edge作成
  7. 想定通り動くかサイトを検証

1. S3作成・サイトのアップロード

サイトのコードを作成

GithubからコードをDL

スクリーンショット 2025-06-22 17.14.52.png

下記よりDLしてください(「Code」→ 「Download Zip」):point_down:(ブランチ:main)

サイトをビルドする

ライブラリをインストール

npm install

サイトの確認

特に何も変更しないで大丈夫です。

npm run dev

http://localhost:3000/で下記が表示されます。

スクリーンショット 2025-06-22 20.11.09.png

サイトのビルド

ちゃんとサイトが表示されているのを確認したらビルドします。

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/の中身を全てアップロードします。

スクリーンショット 2025-06-22 20.36.25.png

✅ ここまででS3作成・サイトアップロードの作業は完了です!

2. CloudFront作成

ディストリビューションを作成

CloudFrontに移動して「ディストリビューションを作成」します。

スクリーンショット 2025-06-22 20.43.40.png

Get started

  • Distribution name
    • employee-site

その他そのままで「Next」します。

スクリーンショット 2025-06-22 20.38.43.png

Specify origin

  • Origin type
    • Amazon S3
  • Origin
    • 先ほど作成したバケットを選択する
    • 例)employee-site-jiroyoyogi.s3.ap-northeast-1.amazonaws.com

その他そのままで「Next」します。

スクリーンショット 2025-06-22 20.39.49.png

Enable security

  • Web Application Firewall(WAF)
    • セキュリティ保護を有効にしないでください

「Next」します。

スクリーンショット 2025-06-22 20.40.18.png

Review and create

「Create distribution」します。

スクリーンショット 2025-06-22 20.40.40.png

スクリーンショット 2025-06-22 20.56.10.png

キャッシュを無効にする

今回の構成ならば不要かとは思いますが認証系のコンテンツはキャッシュさせないのが基本かなと思うので、コンテンツがキャッシュされないように変更します。

「ビヘイビア」タブで下記のように選択して「編集」します。

スクリーンショット 2025-06-22 22.01.24.png

「キャッシュキーとオリジンリクエスト」の「キャッシュポリシー」を「CachingDisabled」に変更します。変更したら「Save changes」します。

スクリーンショット 2025-06-22 22.01.46.png

サイトの確認

「一般」タブで「最終変更日」が更新されたのを確認します。

スクリーンショット 2025-06-22 22.09.06.png

「ディストリビューションドメイン名」をコピーしてブラウザで表示確認します。

スクリーンショット 2025-06-22 21.07.55.png

AccessDenideと表示されたらドメインに/index.htmlを付けて再度アクセスして下さい。

例)https://abcdefg123.cloudfront.net/index.html

CloudFrontではデフォルトのインデックスドキュメントが設定されていないため、明示的に index.html を指定する必要があります(後ほどLambda@Edgeで補います)。

ローカルホストで表示してたサイトが現れたら成功です。

スクリーンショット 2025-06-22 21.15.20.png

✅ ここまででAWS・マネコンでの設定は完了です!

3. Okta管理画面でアプリケーション作成

下図のアプリケーション画面にて「アプリ統合」します。

スクリーンショット 2025-06-22 21.24.32.png

  • サインイン方法
    • OIDC -OpenID Connect
  • アプリケーションタイプ
    • Webアプリケーション

上記のように設定して「次へ」移動します。

スクリーンショット 2025-06-22 21.24.50.png

  • アプリ統合名

employee site edge

  • サインインリダイレクトURI

例)https://12345abcd.cloudfront.net/callback

※CFのドメインはご自身のもので置き換えて下さい。

  • サインアウトリダイレクトURI

例)https://12345abcd.cloudfront.net/

末尾「/」ありでお願いします。無くてもいけると思いますが未検証です。

  • その他

そのままでOKです。

スクリーンショット 2025-06-22 21.27.24.png

  • アクセス制御
    • Organizationの全員にアクセスを許可

扱い方を理解してる方は「選択されたグループにアクセスを制御」でも大丈夫です。

スクリーンショット 2025-06-22 21.27.33.png

「保存」しましょう。

追加の検証としてPKCEを要求

「編集」してProof Key for Code Exchangeの項目にて「追加の検証としてPKCEを要求」にチェックを入れます。チェックを入れたら「保存」します。

スクリーンショット 2025-06-22 21.40.42.png

✅ Oktaのアプリケーション作成が出来ました!次に認可サーバーを作成して行きます!

4. Okta管理画面で認可サーバー追加

左ペイン「セキュリティ」→「API」を選択して画面移動します。「認可サーバーを追加」

スクリーンショット 2025-06-22 21.47.25.png

  • 名前
    • employee-site
  • オーディエンス
    • api://employee-site

「保存」します。

スクリーンショット 2025-06-22 21.49.14.png

「アクセスポリシー」タブに移動し「ポリシーを追加」します。

スクリーンショット 2025-06-22 21.50.13.png

  • 名前
    • employee-site
  • 説明
    • employee-site
  • 次に割り当てる:
    • 「employee site edge」を選択(※)

(※)入力欄に「e」と入力すると選択肢が表示されます

先ほど作成したアプリケーションと認可サーバーとの紐付けをしています。

スクリーンショット 2025-06-22 21.51.16.png

「ポリシーを作成」します。

スクリーンショット 2025-06-22 21.51.23.png

「ルールを追加」します。ユーザーに発行されるアクセストークンの有効期限などのルール設定をします。

スクリーンショット 2025-06-22 21.54.59.png

  • ルール名
    • authenticated rule

デフォルトのままで「ルールを作成」します。

スクリーンショット 2025-06-22 21.55.30.png

✅ お疲れ様です!Oktaでの設定は以上になります!!

5. Lambda@Edgeのロジック・OIDCの認証フローの解説

GithubからコードをDL

スクリーンショット 2025-06-23 21.51.56.png

下記よりDLしてください(「Code」→ 「Download Zip」):point_down:(ブランチ: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の画面でログインするなど認証の段階が進む中で、乗っ取りなどされていないかの検証をする際に上記の文字列たちを使います。

検証のイメージ(※)は次のような感じ。

各文字列を最初に作り、認証の段階が進む中で伝言ゲーム(バケツリレー)する。最初と最後が同じならば、途中でおかしなことは起きていない。

スクリーンショット 2025-06-23 23.16.18.png

念押しますがあくまでイメージです。

PKCEは文字列が2つ必要

上記コードでPKCEのみ2つ文字列を作成しています。

  1. ランダムな文字列
  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で認証されるまでのイメージ

🔑 本図で使っている「鍵」アイコンは、statenoncecode_verifier のような「検証用のランダム値」を視覚的に表そうとトライしたものです。暗号学的な「鍵(key)」とは別物です。温かい目で見ていただけたら幸いです。

フロー.png

ログインコールバック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をセットします。

スクリーンショット 2025-06-24 8.26.25.png

CLIENT_ID

3.で作成したアプリケーションのクライアントIDをセットします。

スクリーンショット 2025-06-24 9.43.20.png

CLIENT_SECRET

↑の画面を少しスクロールすると「クライアントシークレット」の項目があります。クリップボードをクリックしてコピーしてセットします。

スクリーンショット 2025-06-24 9.42.18.png

CLOUD_FRONT_DOMAIN

2.で作成したCloudFrontの「ディストリビューションドメイン名」をコピーしてセットします。

スクリーンショット 2025-06-24 8.34.30.png

セット後のイメージ

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に移動します。リージョンを「バージニア北部」に切り替えます。

スクリーンショット 2025-06-24 10.09.36.png

※Lambda@Edgeはバージニア北部でしか作れません。

関数の作成

  • 関数名
    • employee-site-lambda-edge
  • ランタイム
    • Node.js22.x
  • アーキテクチャ
    • x86_64

デフォルトの実行ロールの変更

  • 実行ロール
    • AWS ポリシーテンプレートから新しいロールを作成
  • ロール名
    • employee-site-lambda-edge-role

下図のように「基本的なLambda@Edgeのアクセス権限(CloudFrontトリガーの場合)」を選択

スクリーンショット 2025-06-24 10.13.45.png

上記のように設定した上で「関数の作成」をします。

zipアップロード

下図の右下「.zipファイル」を選択

スクリーンショット 2025-06-24 10.15.47.png

先ほど作成したfunction.zipをアップロードします。

スクリーンショット 2025-06-24 10.16.03.png

コードソースが更新されました。

スクリーンショット 2025-06-24 10.18.53.png

タイムアウト時間を延長する

「設定」タブ→「一般設定」→「編集」します。

タイムアウトを 5秒 に設定します。これはビューワーリクエストにつけるLambda@Edgeの最大タイムアウト時間です。

これをしないとコールドスタートの場合にタイムアウトになる可能性が増えます。ちなみに、タイムアウトの場合は503エラーの画面が表示されます。

スクリーンショット 2025-06-24 21.34.42.png

Lambda@Edgeへのデプロイ

下図の右上の「アクション」→「Lambda@Edgeへのデプロイ」を選択します。

スクリーンショット 2025-06-24 10.19.14.png

  • オプションを選択
    • 新しいCloudFrontトリガーの設定
  • ディストリビューション
    • 作成したCloudFrontを選択
  • キャッシュ動作
    • *
  • CloudFrontイベント
    • ビューアーリクエスト

設定出来たら「デプロイ」します。

スクリーンショット 2025-06-24 10.20.13.png

CloudFrontへ移動

下図の右の「最終変更日」に日時が入るのを待ちます。

スクリーンショット 2025-06-24 10.20.42.png

スクリーンショット 2025-06-24 10.27.10.png

✅ あとは確認するのみです!!

想定通り動くかサイトを検証

サイトにアクセス

「ディストリビューションドメイン名」をコピーしてアクセスするとOktaのログイン画面に飛ばされます。

スクリーンショット 2025-06-24 10.32.36.png

ログインするとS3に置いたサイトが表示されました!

スクリーンショット 2025-06-24 10.35.44.png

URLに/index.htmlがちゃんと追加されているのも確認出来ます。

cookieのIDトークンを確認

開発者ツールを開き「Application」タブ→「Cookies」を開きます。

スクリーンショット 2025-06-24 23.24.09.png

base64でエンコードされたIDトークンが入ってることが確認出来ます。

トークンの中身を確認

最後にトークンの中身も確認しておきます。

「Cookie Value」をコピーした上でhttps://jwt.io/ にアクセス、「Encoded value」の入力欄に貼り付けます。

スクリーンショット 2025-06-26 18.14.53.png

スクリーンショット 2025-06-26 18.16.31.png

トークンの中にメールアドレスなどが含まれてることが確認出来ます。

スクリーンショット 2025-06-26 18.18.47.png

完成

この記事での実装は以上となります。

もし、さらにユーザー名も表示したいという方いらっしゃいましたら、下記動画でお伝えしてるので、是非ご活用下さい。51分28秒あたりが該当部です。

おわりに

これまで、OktaとCognitoを連携させてS3ホスティングのサイトにログイン認証を加える構成は何度か試してきました。ただ、「Cognitoを使わず、Okta単体で完結できないか?」と試してみたい気持ちがあり、今回の構成を試してみることにしました。

また、OIDCでよく出てくる state や nonce、PKCE といった用語について、これまでは何となく「必要そうなもの」として扱ってきましたが、実際に一から実装してみることで、それぞれの役割や検証方法について深く理解できたのが大きな収穫です。

Lambda@Edgeの扱いやOIDCフローなど、細かいところでハマることも多かったですが、それだけに得られる学びも多く、やって良かったと思える構成でした。

ここまで読んでいただきありがとうございましたー!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?