CloudFront で Lambda@Edge を使って OpenID Connect (OIDC) 認証

![概要](https://hirose-public.s3-ap-northeast-1.amazonaws.com/qiita/2021/03/13/380a4871-3997-411d-8b7d-ad5ac3282b3f.png)

CloudFront と S3 を使ってサーバーレスなウェブサイトを構築し、特定のユーザーにのみ公開するために Google, GitHub など外部の認証プロバイダ (IdP) を使って認証をおこなう方法です。CloudFront ではリクエスト中継時に Lambda 関数を実行し認証などの処理をおこなうことができるため、それを使うことにします。

## OpenID Connect による認証シーケンス

CloudFront にアクセスした際のシーケンスは以下のようになります。

![](https://hirose-public.s3-ap-northeast-1.amazonaws.com/qiita/2022/09/11/41a028e6-084d-4ddb-93af-de43eb49e003.png)

## 構築手順

OpenID Connect (OIDC) による認証をおこなう Lambda 関数を [Widen/cloudfront-auth](https://github.com/Widen/cloudfront-auth) というツールを使って生成します。様々な認証プロバイダに対応していますが、ここでは Google で認証し、特定ドメインのメールアドレスのユーザーのみをアクセス許可してみます。

※補足) このプロジェクトは 2022/08 にメンテナンスが終了し、アーカイブされています。そのままだと Lambda のランタイムがサポート終了済みの Nodejs 10.x となっており、デプロイできません。幸いランタイム設定を新しいバージョンに変更するだけで動作しました。

### 前提条件

- CloudFront によるウェブサイトが構築済みであること。

### 設定項目

以下の設定項目が必要となる。

- **Client ID/Sercet**: 認証プロバイダ(IdP)で認証をおこなうために払い出したクライアント用のIDとシークレット。この場合 CloudFront で動作する Lambda 関数が OIDC のクライアントという位置付け。
- **Domain Name**: CloudFront でホストしているウェブサイトの URL の host (CNAME を設定していない場合は `xxxxxxxxxxxxxx.cloudfront.net`)
- **Callback URL**: 認証プロバイダ(IdP)からの認証結果を受け取り、処理するためのエンドポイント。「`https://(Domain Name)/_callback`」など、実際にサイト内に存在するファイルとは重複しないものを適当に指定する。
  例) `https://xxxxxxxxxxxxxx.cloudfront.net/_callback`
- **Distribution Name**: AWS に デプロイする際の名称 (任意)

Client ID/Secret は、認証プロバイダ(IdP)で払い出しをおこなう。Google の場合は以下の手順。

 1. [Google API Console](https://console.developers.google.com/) にアクセス
 1. プロジェクトを作成
 1. 「Credential」からクライアントを作成
     1. 「Create credentials」→「OAuth client ID」
     1. 「Application type」に「Web application」を選択
     1. 「Name」に適当な名前を入力 (例: `cloudfront-auth`)
     1. 「Authorized redirect URIs」に「Callback URL」を入力
     1. 「Client ID」と「Client secret」が発行されるので、メモしておく

![Google API Credentials 一覧画面](https://hirose-public.s3-ap-northeast-1.amazonaws.com/qiita/2019/12/08/3ca70fbb-6fff-4d21-ba23-3ca32af31175.png)

### 1. チェックアウト

[Widen/cloudfront-auth](https://github.com/Widen/cloudfront-auth) をチェックアウトします。

```shell
$ git clone https://github.com/Widen/cloudfront-auth.git
$ cd cloudfront-auth
```

### 2. Lamda 関数の生成

設定スクリプトを実行して必要事項を入力すると、それ用の Lambda 関数が生成されます。

 1. 設定スクリプトを実行

    ```shell
    $ ./build.sh
    ```

 1. Lambda のディストリビューション名を指定する。(任意の名前)

    ```shell
    >: Enter distribution name:  cloudfront-auth-xxx (例)
    ```

 1. 使用する認証プロバイダ(IdP)を選択

    ```shell
    >: Authentication methods:
        (1) Google
        (2) Microsoft
        (3) GitHub
        (4) OKTA
        (5) Auth0
        (6) Centrify
        (7) OKTA Native
    
        Select an authentication method:  1
    ```

 1. クライアント認証情報を入力

    ```shell
    >>: Client ID:  …「Client ID」を入力する
    >>: Client Secret:  …「Client secret」を入力する
    ```

 1. Callback URL を入力

    ```shell
    >>: Redirect URI:  https://xxxxxxxxxxxxxx.cloudfront.net/_callback (例)
    ```

 1. ログインユーザーのドメイン名を入力。これに後方一致するメールアドレスのユーザーのみアクセスが許可される。

    ```shell
    >>: Hosted Domain:  gmail.com
    ```

 1. 認証の有効期限を入力
    デフォルトの 0 だとすぐに期限切れとなって全くアクセスできないので注意。(やらかした)

    ```shell
    >>: Session Duration (hours):  (0) 24
    ```

 1. 認可の設定

    ```shell
    >>: Authorization methods:
       (1) Hosted Domain - verify email's domain matches that of the given hosted domain
       (2) HTTP Email Lookup - verify email exists in JSON array located at given HTTP endpoint
       (3) Google Groups Lookup - verify email exists in one of given Google Groups
    
       Select an authorization method:  1
    ```

    アクセス可能なユーザーを設定する方法として、以下の 3 つから選択することができる。(Google の場合)

     1. ホスティングドメインに指定したドメインのメールアドレスのユーザーのみアクセスを許可 (今回はこれを選択)
     2. 特定の URL からダウンロードした JSON に書かれたメールアドレスのユーザーのみアクセスを許可
     3. 特定の Google グループのユーザーのみアクセスを許可

 1. 完了
    生成された関数は、distributions/{distribution name}/{distribution name}.zip に出力される。

    ```shell
    Done... created Lambda function distributions/cloudfront-auth-xxx/cloudfront-auth-xxx.zip
    ```

### 3. Lambda 関数のデプロイ

生成された Lambda 関数を AWS にデプロイします。が、その前に、template.yaml を修正します。

 1. template.yaml の `{distribution_name}` をディストリビューション名に書き換え

    ```shell
    $ DISTRIBUTION_NAME=cloudfront-auth-xxx (例)
    $ sed -i.bak "s/{distribution_name}/${DISTRIBUTION_NAME}/g" template.yaml
    ```

 1. template.yaml のランタイム設定を Node.js 16 に書き換え

    ```shell
    $ sed -i.bak "s/nodejs10\.x/nodejs16\.x/" template.yaml
    ```

デプロイは [手動](https://github.com/Widen/cloudfront-auth/wiki/Manual-Deployment) または [AWS SAM](https://aws.amazon.com/serverless/sam/) のいずれかでできます。SAM を使った場合は、以下を実行します。

```shell
$ sam deploy --guided ★2度め以降は sam deploy のみでOK
Stack Name [sam-app]: cloudfront-auth-xxx (例) …CFnスタック名を指定 (任意)
AWS Region [us-east-1]: ★Lambda@Edge 関数は us-east-1 にデプロイする必要がある
... (あとはお好み) ...
```

### 4. CloudFront ディストリビューションに Lambda@Edge を設定

 1. ホスティングしている CloudFront ディストリビューションの設定を開く

 1. Default Behaviors を Edit

 1. 「Lambda Function Associations」に以下を追加

    |CloudFront Event|Lambda Function ARN|Include Body|
    |:--|:--|:--|
    |Viewer Request|arn:aws:lambda:us-east-1:(アカウント番号):function:(関数名):1|チェックしない|

「Lambda Function ARN」にはバージョン付きの ARN を指定する。更新してデプロイし直した場合、Lambda 関数のバージョンが上がっていくので、その都度ここも変更する必要があります。

CloudFront の設定反映は、しばらく時間がかかるので気長に待ちましょう。

![Lambda@Edge 設定画面](https://hirose-public.s3-ap-northeast-1.amazonaws.com/qiita/2022/09/11/210a57ab-caba-4edd-8c3a-896d8461ea1b.png)

## FAQ (やらかしメモ)

### 認証完了後に無限ループする

認証完了後に元のページにリダイレクトされるが、すぐに再度認証ページにリダイレクトされ、リクエストが無限ループしてしまう。実際にはブラウザが検知して数回繰り返したあとエラー画面になる。有効期限を 0 にしてしまうと、認証完了直後に期限切れとなるため、こうなってしまいます。

### 401 でページが参照できない

認証完了後、画面に「User (メールアドレス) is not permitted.」というエラーメッセージが表示される場合、認証はうまくいっているが認可でエラーになっている。ログインしたメールアドレスに対してアクセスが許可されているかを確認しましょう。