🕋

Lambda@Edgeを利用してCloudFrontにcognito認証をかける

2022/07/09に公開

はじめに

フロントエンドのアプリケーションに、楽に・意識せずに認証をつけたいと思いませんか?
例えば、CloudFrontで動作するアプリケーションに認証をつける場合などです。

最近は、CognitoやAmplifyのライブラリを利用するとフロントエンドのコードのみで認証を構築できます。
その場合、次のような問題への対処を考える必要があります。

  1. ページにアクセスすると、JShtmlのコンテンツそのものは必ず露出してしまう
  2. 意図せずに見せたくないページが見えてしまうことがある

上記のような問題を解決するために、インフラ側に認証の機能を寄せます。
そうすることでセキュリティを高めつつ、アプリケーションに認証の機能を持たせることができます。

環境・料金

環境

  • macOS: 12.4
  • Node.js: 16.15
  • AWS CDK: 2.28.0

料金

今回利用するAWSのサービスは、主に以下の4つです。

  • CloudFront
  • Lambda@Edge
  • Cognito
  • Route 53

これらのサービスを利用する場合は、サイトへのアクセスが100万回、
ユーザー数も10万人程度にならないと課金は発生しません。
唯一、Route 53でドメインを利用する必要があり、そこだけ料金がかかります。
取得するドメインによっても変わりますが、年額で1500円〜程度です。

できるもの

以下のAWSリソースをCDKで作成します。

参考にも載せてありますが、本記事はAWS公式の記事で公開されている
GitHubのコードを参考に作成しています。

参考の内容をほぼ流用するために、自分でus-east-1にデプロイしてます。
experimentalパッケージを利用すればより簡単にデプロイできることはできますが、
現時点でTypeScriptをビルドできないのでそうしています。

メリット

メリットは大きく2つあります。

1つ目は、特にSPAだと大きなメリットなのですが、
コンテンツのダウンロードそのものに認証が必要ということです。

通常のアプリケーションはhtmlやJSのファイルがダウンロードされて
そのダウンロードされたJSの挙動によってCognitoの認証などをかけることが一般的です。
そのため、認証をする前にhtmlやJSなどのコンテンツはダウンロードされてしまっています。
悪用される可能性は低いとは思いますが、コンテンツのダウンロードは避けられないです。

一方、この方法を利用すれば、S3に置かれているリソースに
CloudFront経由でダウンロードする前にCognitoの認証がかかります。
これは、htmlやJSのコンテンツがダウンロードすらされないということです。
そのため、安全にコンテンツを管理できます。

2つ目は、認証まわりの実装をインフラに寄せることができます。
フロントエンドやバックエンドではなく、インフラに認証の仕組みを入れることができ、
実装の漏れなどが、無くせる点は大きいと思います。
(Amplifyのパッケージを利用すれば、実装の漏れはあまり起きなさそうですが…)。
ただし、フロントエンドに認証を実装する場合に比べて
ユーザーの情報とかは取得しづらくなるかもしれないです。

デメリット

セキュリティ的にはかなりメリットはあるのですが、
この方法のデメリットは、CognitoのHostedUI以外は使いづらい点です。

HostedUIでも見た目のカスタマイズや日本語化が簡単にできればいいのですが、あまり簡単ではありません。見た目などを重視するサービスを利用したい場合は、この方法は現時点ではあまり有効ではないです。

一応、この方法を利用しつつカスタムしたログイン画面の表示はできます。それは、CognitoのHostedUIに代わる認証サーバーを作成することです。ここでは詳細は記述しませんが、AWS公式の記事に書かれているリダイレクト先をCognitoHostedUIから独自の認証サーバーを作成すれば回避できます。

メリットとデメリットはもちろんあるので、使いたいサービスやユーザーの要望に応じて使い分けてください。

準備

Route 53ACMを設定する必要があります。

ドメインとACMは、以下の記事の内容が設定されていれば大丈夫です。

https://zenn.dev/gsy0911/articles/da47b660b7dd2b7d1ae7

また、本記事では説明しませんが、環境にCDKv2を利用する準備も必要です。

プログラム

GitHub

プログラムは以下のリポジトリで公開してあります。

https://github.com/gsy0911/zenn-cloudfront-cognito/tree/v1

実際に動かしたい場合はcloneして実行してみてください。

ディレクトリ構成

CDKv2なのでlib/ディレクトリに今回利用するStackが書かれています。
この記事で利用するStackは以下の4つです。

  • CloudFrontCognitoStack.ts
  • CognitoStack.ts
  • LambdaEdgeStack.ts
  • XRegionParam.ts

XRegionParamの動作についてはこちらの記事をご覧ください。

root/infrastructure/
├── bin/
│  └── index.ts
├── cdk.json
├── jest.config.js
├── lib/
│  ├── example_html/
│  ├── lambda/
│  ├── CloudFrontCognitoStack.ts
│  ├── CloudFrontStack.ts
│  ├── CognitoStack.ts
│  ├── index.ts
│  ├── LambdaEdgeStack.ts
│  ├── params.example.ts
│  └── XRegionParam.ts
├── package-lock.json
├── package.json
└── tsconfig.json

ソースコードの中身

特にソースコードの説明はしないです。
詳しくはGitHubの方をみてください。

重要なのは、lib/CloudFrontCognitoStack.tslib/LambdaEdgeStack.tsの2つのみです。
lib/lambda以下のファイルも重要なのですが、参考元のファイルのほぼそのままで
見にくい改行などをしただけなので解説することは特にないです。

デプロイ準備

デプロイをするために2つのファイルの準備が必要なので、例ファイルからコピーします。

$ cd infrastructure
$ cp lib/params.example.ts lib/params.ts
$ cp lib/lambda/shared/configuration.example.json lib/lambda/shared/configuration.json

それぞれのファイルの使うところと、パラメータの設定タイミングは次の通りです。

ファイル名 用途 パラメータ設定タイミング
params.ts CDKのデプロイ 最初のデプロイをする前
configuration.json Lambda@Edgeのパラメータ設定 CognitoのStackのデプロイ後

params.tsの編集

params.tsはこのタイミングで編集します。
domaincertificateに関しては、準備のところで得た値を入力してください。
それ以外の値は、基本的に設定したい値で大丈夫です。

subDomain1subDomain2の名前を今回は固定していますが、任意の値で大丈夫です。

params.ts
import { ICloudFrontStack } from './CloudFrontStack';
import { ICognitoStack } from './CognitoStack';
import {
  Environment
} from 'aws-cdk-lib';

- const newlyGenerateS3BucketBaseName: string = "newly-generate-s3-bucket-base-name"
- const accountId: string = "00001111222"
- const domain: string = "your.domain.com"
- const referer: string = "referer-using-s3-cognito"
+ const newlyGenerateS3BucketBaseName: string = "xxxxxx"
+ const accountId: string = "33334444555"
+ const domain: string = "xxx.xxxxx.com"
+ const referer: string = "as-you-like"
const subDomain1: string = `app1.${domain}`
const subDomain2: string = `app2.${domain}`
- const cognitoDomainPrefix: string = "cognito-unique-domain-example"
+ const cognitoDomainPrefix: string = "xxxxxx"

export const paramsCloudFront1Stack: ICloudFrontStack = {
  s3: {
    bucketName: `${newlyGenerateS3BucketBaseName}-1`,
    referer: referer
  },
  cloudfront: {
-     certificate: `arn:aws:acm:us-east-1:${accountId}:certificate/{unique-id}`,
+     certificate: `arn:aws:acm:us-east-1:${accountId}:certificate/{your-id-here}`,
    domainNames: [subDomain1],
    route53DomainName: domain,
    route53RecordName: subDomain1
  }
}

// ~~ 後略 ~~

params.tsの準備が終わったら、デプロイの準備は一旦完了です。

デプロイ

3回以下の順番にデプロイする必要があります。

  1. CognitoStack
  2. LambdaEdgeStack
  3. CloudFrontCognitoStack

1. CognitoStackのデプロイ

Cognitoのデプロイの準備は先ほど終わっているので、
そのままデプロイできます。

cdk deploy example-cloudfront-cognito-cognito

2.1 LambdaEdgeStackのデプロイ準備

Cognitoのデプロイが終わったら、configuration.jsonを編集します。
Cognitoで利用するappClientの種類によって、設定する値が異なります。

appClientは、clientSecretがあるかないかで以下の2種類に分けています。

  • publicClient: clientSecretが生成されていない
  • privateClient: clientSecretが生成されている

今回の例の場合だと、どちらのclientを利用しても大丈夫です。
以下のdiffをみながらjsonの編集をしてください。

configuration.json
{
-  "userPoolArn": "arn:aws:cognito-idp:ap-northeast-1:{accountId}:userpool/{poolId}",
+  "userPoolArn": "arn:aws:cognito-idp:ap-northeast-1:333344445555:userpool/xxxx-xxxx-xxxx",
-   "clientId": "{publicClientId} or {privateClientId}",
+   "clientId": "{publicClientId/privateClientId} どちらかの値を設定",
-   "clientSecret": "null or {clientSecret}",
+   "clientSecret": null,  # publicClientIdの場合
+   "clientSecret": "xxxx-xxxx",  # privateClientIdの場合
  "oauthScopes": [
-     "aws.cognito.signin.user.admin",  # publicClientIdの値を設定した場合に削除
    "email",
    "openid",
    "phone",
-    "profile"  # publicClientIdの値を設定した場合に削除
  ],
-   "cognitoAuthDomain": "{your-cognito-domain-prefix}.auth.ap-northeast-1.amazoncognito.com",
+   "cognitoAuthDomain": "xxxx-xxxx.auth.ap-northeast-1.amazoncognito.com",
  "redirectPathSignIn": "/oauth2/idpresponse",
  "redirectPathSignOut": "/logout",
  "signOutUrl": "/signout",
  "redirectPathAuthRefresh": "/refreshauth",
  "cookieSettings": {
    "idToken": "",
    "accessToken": "",
    "refreshToken": "",
    "nonce": ""
  },
  "mode": "staticSiteMode",
  "httpHeaders": {
    "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self' https://code.jquery.com https://stackpath.bootstrapcdn.com; style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com; object-src 'none'; connect-src 'self' https://*.amazonaws.com https://*.amazoncognito.com",
    "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
    "Referrer-Policy": "same-origin",
    "X-XSS-Protection": "1; mode=block",
    "X-Frame-Options": "DENY",
    "X-Content-Type-Options": "nosniff"
  },
  "logLevel": "debug",
-   "nonceSigningSecret": "{nonceSigningSecret}",
+   "nonceSigningSecret": "{英数の16文字程度のランダムな値}",
  "cookieCompatibility": "amplify",
  "additionalCookies": {},
  "requiredGroup": ""
}

2.2 LambdaEdgeStackのデプロイ

configure.jsonの編集が終わったので、デプロイします。

cdk deploy example-cloudfront-cognito-lambda-edge

us-east-1にLambdaがデプロイされます。

そして、ap-northeast-1のSSMにパラメータが設定されます。

SMMに設定されているのは、Lambdaのバージョンです。

このバージョンを発行することで、CloudFrontにLambda@Edgeとして付与できます。

3. CloudFrontStackのデプロイ

最後のデプロイです。
S3のバケット作成とCloudFrontのデプロイを実行します。
CloudFrontのディストリビューションが実行されるので、10分ほど時間がかかります。

cdk deploy example-cloudfront-cognito-cloudfront-cognito-1

また、次のデプロイは任意ですが、動作確認の時に全ての挙動を確認したい場合にはデプロイをしてください。

cdk deploy example-cloudfront-cognito-cloudfront-cognito-2

これで全てのデプロイが終わったので、挙動を確認します。

動作確認

CloudFrontにCognitoが設定されたか確認していきます。

ユーザーの作成

ここは、AWSのコンソールからCognitoのユーザーを作成します。
以前のこの記事と同じように作成していきます。

今回作成したuser-poolというユーザープールを選択します。

新規のユーザーを作成します。

赤枠のところを全て埋めて新しいユーザーを作成します。
この時、メールアドレスも埋めるようにしてください。

ユーザーが作成されてこの画面になったら作成は完了しました。

サインイン

params.tsで設定してあるsubDomain1にアクセスします。
特に変更していなかった場合、app1.{your.domain.com}になります。
(画像ではデプロイの都合上app3.{your.domain.com}になっています)。

app1にアクセスすると、cognitoのHostedUIにリダイレクトされ、サインイン画面が出てきます。この記事と同じように、初回ログイン時にパスワードを再設定してサインインしてください。

この時、JWTはCookieに保存されています。
Chromeの検証ツールを開いて、ApplicationからCookiesの項目で確認できます。

サインアウト

サインアウトはapp1.{your.domain.com}/singoutへアクセスします。すると、JWTが無効化されてリロードすると再びサインイン画面に飛ばされます。

挙動の理解

今回は説明しませんが以下の項目について知ると、紹介している動作の理解を深めることができると思います。

  • 認証・認可の違い
  • OIDC
  • リダイレクト
  • Cookie
  • PKCE
  • JWT

後片付け

検証が終わったらリソースの削除をしましょう。デプロイした逆の順番で、リソースを削除していきます。唯一気をつける点として、Lambda@Edgeの削除に1時間ほどかかります。ドメイン以外は課金されないので、忘れないようにだけしてください。

おわりに

認証の機能を、インフラ側に寄せる方法をCDKで実装してみました。参考にしたのは2019年のAWSの記事ですが、現在でも有用だなと感じました。

記事の内容には書きませんでしたが、GitHubにあるコードには認証機能がないCloudFrontもあるので、デプロイして比較してみてください。

参考

GitHubで編集を提案

Discussion