Lambda@Edgeを利用してCloudFrontにcognito認証をかける
はじめに
フロントエンドのアプリケーションに、楽に・意識せずに認証をつけたいと思いませんか?
例えば、CloudFrontで動作するアプリケーションに認証をつける場合などです。
最近は、CognitoやAmplifyのライブラリを利用するとフロントエンドのコードのみで認証を構築できます。
その場合、次のような問題への対処を考える必要があります。
- ページにアクセスすると、
JS
やhtml
のコンテンツそのものは必ず露出してしまう - 意図せずに見せたくないページが見えてしまうことがある
上記のような問題を解決するために、インフラ側に認証の機能を寄せます。
そうすることでセキュリティを高めつつ、アプリケーションに認証の機能を持たせることができます。
環境・料金
環境
- 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 53
とACM
を設定する必要があります。
ドメインとACMは、以下の記事の内容が設定されていれば大丈夫です。
また、本記事では説明しませんが、環境にCDKv2を利用する準備も必要です。
プログラム
GitHub
プログラムは以下のリポジトリで公開してあります。
実際に動かしたい場合は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.ts
とlib/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はこのタイミングで編集します。
domain
とcertificate
に関しては、準備のところで得た値を入力してください。
それ以外の値は、基本的に設定したい値で大丈夫です。
subDomain1
とsubDomain2
の名前を今回は固定していますが、任意の値で大丈夫です。
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回以下の順番にデプロイする必要があります。
- CognitoStack
- LambdaEdgeStack
- 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の編集をしてください。
{
- "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もあるので、デプロイして比較してみてください。
Discussion