【AWS×Java】SAMで冷蔵庫管理アプリを構築 #3 Cognito編
はじめに
当記事の最終ゴールについては以下の記事をご確認ください。
今回のゴール:Cognito User Poolでログインできる
API Gatewayを User Pool Authorizerで保護できる
フロント対応に向けたCORS対応ができる
前提読者:AWS初心者、開発初心者
また、今回は以下「【AWS×Java】SAMで冷蔵庫管理アプリを構築 #2 DynamoDB編(CRUD API②)」を実施済みの前提で進めます。
以下を実施することで従量課金が発生します。その点については自己責任でお願いします。無料枠があるが上限があります。削除することで課金を止めることが可能です。
また、本記事は学習ログです。詳細は公式ドキュメントを適宜参照してください。
アーキテクチャ概要
今回は以下赤枠部分を完成させます。
図1: サーバレス構成の全体像
「Infrastructure Composer/ Application Composer」でtemplate.yamlに追加
前回同様、backendディレクトリの配下に「template.yaml」があるので右クリックをして
「Open with Infrastructure Composer」を選択してください。
すると以下のような図が表示されているかと思います。
(旧称がInfrastructure Composerらしく、私は「Infrastructure Composer」と表示されていましたが、「Application Composer」と表示されている方はそちらを選択してください)
-図2 Application Composer(前回状態)
まずはCognito UserPoolとCognito UserPoolClientを追加し、さらに接続させます。
-図3 Application Composer(Cognito追加後)
さらに以下値を設定してください。
Cognito UserPool
項目名 | 設定値 |
---|---|
論理ID | FridgeUserPool |
ユーザーにサインアップを許可 | チェックを入れる |
Cognito UserPoolClient
項目名 | 設定値 |
---|---|
論理ID | FridgeUserPoolClient |
-図4 Application Composer(設定後)
template.yamlとApp.javaの修正
ここまで愛用してきたInfrastructure Composer(Application Composer)はここからはあまり使えず、あとはtemplate.yamlを直接編集していきます。
①Hosted UIを使うためのクライアント設定とドメイン
当設定の概要をお伝えします。
まず「Hosted UI」というのはCognitoが事前に準備してくれている以下のようなログイン画面のことです。以下でログインしないと冷蔵庫アプリを使えないようにします。今回はメールアドレスでのみログインできるようにしています。
-図5 Hosted UI
次に、「クライアント設定」ですが、これは「どのアプリケーションが登録されたユーザー情報を使うか」の設定になります。今回フロント側はHTML+JavaScriptで超シンプルに作成しますが、ブラウザでその冷蔵庫アプリのURLにアクセスした際の挙動を設定しています。
[ブラウザ(HTML+JavaScript)]
↓ Hosted UI (Cognitoログイン画面)
↓ 認証
[ Cognito User Pool (ユーザーデータ) ]
↓ 認証後、トークン発行
↓ 指定したCallback URLにリダイレクト
最後に「ドメイン設定」の部分ですが、Hosted UIにアクセスするためにはURLが必要です。
Cognito側で「https://◯◯.auth.ap-northeast-1.amazoncognito.com」のような専用ドメインが自動発行されます。
このURLをブラウザで開くと、Cognitoのログイン画面が表示されます。
上記の内容をtemplate.yamlファイルに設定するため、以下の修正を行います。
FridgeUserPool:
Type: AWS::Cognito::UserPool
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
- AliasAttributes:
- - email
- - preferred_username
+ UsernameAttributes:
+ - email
+ AutoVerifiedAttributes:
+ - email
UserPoolName: !Sub ${AWS::StackName}-FridgeUserPool
FridgeUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref FridgeUserPool
+ ClientName: fridge-web
+ GenerateSecret: false # SPAなら必ずfalse
+ AllowedOAuthFlows: [ code ] # PKCE
+ AllowedOAuthFlowsUserPoolClient: true
+ AllowedOAuthScopes: [ openid, email ]
+ SupportedIdentityProviders: [ COGNITO ]
+ CallbackURLs: [ "http://localhost:5173/callback" ] # 後でCloudFrontに置換
+ LogoutURLs: [ "http://localhost:5173" ]
+
+ FridgeUserPoolDomain:
+ Type: AWS::Cognito::UserPoolDomain
+ Properties:
+ Domain: !Sub ${AWS::AccountId}-fridgeapp2-${AWS::Region} # 空いている任意のサブドメイン
+ UserPoolId: !Ref FridgeUserPool
※ CallbackURLsですが、フロント側の設定ができていないので一旦ローカル環境を指定します。
本番のURLは「https」であることが必須ですがhttp://localhostは例外的に許可されています。
最終的にはCloudFrontを使ってどのPCやスマホからもアクセスできるようにします。
その内容は次回更新予定です。
注意:本記事はメールログイン専用のため UsernameAttributes: [email] を採用します。
これは User Pool作成前に設定してください。作成後に属性モードを変更することはできません(Cognito仕様)。既に作成済みの方はスタック削除 or 新しい論理IDで再作成してください。
②API GatewayをCognitoで保護
①の設定によって画面にはログインできなくなったのですが、前回説明したcurlコマンド等を用いて直接DynamoDBにアイテムを追加したり、アイテムを確認することもできます。勝手に冷蔵庫の中身を荒らされると困るので認証情報がないと前回作成したCRUD APIを使用できないようにします。
以下を設定することで全てのAPIメソッドがCognito必須になります。
Globals:
Function:
Timeout: 20
MemorySize: 512
+ Api:
+ Auth:
+ Authorizers:
+ CognitoAuthorizer:
+ UserPoolArn: !GetAtt FridgeUserPool.Arn # 既存のUserPool
+ DefaultAuthorizer: CognitoAuthorizer
③フロント実装に向けたCORS対応
私はこのアプリを一度完成させておりませんが、一番困ったのはCORSかなと思います。私は理解せぬまま進めたのでかなり時間がかかりましたが、一度しっかりと内容を理解したほうがいいかと思います。
以下、AWSサイトのCORSに関する説明をご確認ください。
上記記載の通り、curlコマンド等でAPIの動作を確認する上では考慮不要なのですが、次回のフロントから今回作成したAPIを使用する際にCORSの考慮が必要です。
以下は平たく言うと以下2つの修正をしています。
①フロントのURL「http://localhost:5173」からのアクセスのみ許可する。
②OPTIONSに認証を掛けない(プリフライトは無認可でOKを返す)
Globals:
Function:
Timeout: 20
MemorySize: 512
Api:
+ Cors:
+ AllowOrigin: '''http://localhost:5173'''
+ AllowHeaders: '''Content-Type,Authorization'''
+ AllowMethods: '''GET,POST,PUT,DELETE,OPTIONS'''
Auth:
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt FridgeUserPool.Arn # 既存のUserPool
DefaultAuthorizer: CognitoAuthorizer
+ AddDefaultAuthorizerToCorsPreflight: false # プリフライトの設定
public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
// 「http://localhost:5173」のみアクセスを許可
+ headers.put("Access-Control-Allow-Origin", "http://localhost:5173");
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
.withHeaders(headers);
これでtemplate.yamlとApp.javaの修正は完了です。
ビルド&デプロイ
backendフォルダ配下でビルド&デプロイしてください。
cd backend
sam build
sam deploy
動作確認
認証により保護されていることの確認
デプロイが成功していれば、前回同様、以下を実行して動作確認してください。
前回も実行したGET処理を確認します。
curl.exe https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/items
以下のような結果が返ってきたら成功です。
(認証されていないので中身が見れなくなっています。)
{"message":"Unauthorized"}
認証済であればAPIが使えることの確認
少し面倒なのですが、認証情報も付与して確認する方法をお伝えします。
長いので省略してますが、もしうまく設定できていない場合は次回のフロント編で確実につまづくので可能であればこのタイミングで以下の確認をしておいてください。
確認方法
手順①
マネジメントコンソールにて「Cognito」を開き、必要情報を確認します。
・ユーザープールを選択
・以下から今回作成したユーザープールを選択
・左側の選択欄からブランディング→ドメインにアクセス
・以下のドメインをコピー
・アプリケーション→アプリケーションクライアントを選択
・以下から今回作成したアプリケーションクライアント名を選択
・以下クライアントIDをコピー
手順②
以下を実行し、「code_verifier=」の後ろに表示される部分をメモしておく。
$bytes = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$verifier = [Convert]::ToBase64String($bytes).Replace('+','-').Replace('/','_').TrimEnd('=')
$sha256 = [Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($verifier))
$challenge = [Convert]::ToBase64String($sha256).Replace('+','-').Replace('/','_').TrimEnd('=')
"code_verifier=$verifier"
"code_challenge=$challenge"
手順③
①で取得したドメインとリージョンとクライアントIDを以下に貼り付けます。
ドメイン<DOMAIN>には以下の赤枠部分を貼り付けて実行してください。(「https://」の後ろから「-ap-northeast-1」の前まで)
$domain = "<DOMAIN>"
$region = "<REGION>"
$clientId = "<CLIENTID>"
$redirect = "http://localhost:5173/callback"
$authUrl = "https://$domain.auth.$region.amazoncognito.com/oauth2/authorize?response_type=code&client_id=$clientId&redirect_uri=$([uri]::EscapeDataString($redirect))&scope=openid+email&code_challenge=$challenge&code_challenge_method=S256"
Start-Process $authUrl
これを実行すると以下のようなサイン画面が出てくると思いますのでサインアップ画面に移動し、使用できるメールアドレスを登録してください。登録すると6桁の認証コードが飛んでくると思うのでそれを入力してください。
-図6 Hosted UI(サインイン)
-図7 Hosted UI(サインアップ)
登録が完了し、6桁の認証コードも認証されると「このサイトにアクセスできません」という画面になると思いますが間違っていません。その画面のアドレスバーの「code=」以降の部分をコピーしてください
-図8 Hosted UI(コードのコピー)
手順④
上記①~③で取得したドメイン・クライアントID・コード・VERIFIERを以下に貼り付けます。
ドメイン<DOMAIN_ALL>にはコピーしたドメインすべてを貼り付けてください。(以下部分すべて)
$tokenUrl = "<DOMAIN_ALL>/oauth2/token" #手順①のドメイン
$clientId = "<CLIENTID>" #手順①のクライアントID
$redirect = "http://localhost:5173/callback"
$code = "<CODE>" #手順③のコード
$verifier = "<VERIFIER>" #手順②の「code_verifier=」の後ろ
$body = "grant_type=authorization_code" +
"&client_id=$clientId" +
"&redirect_uri=$([uri]::EscapeDataString($redirect))" +
"&code=$code" +
"&code_verifier=$verifier"
try {
$resp = Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body $body
"`n=== token response ==="
$resp | Format-List
$idToken = $resp.id_token
"`n[id_token length] $($idToken.Length)"
} catch {
"`n=== error ==="
$_.Exception.Response.GetResponseStream() | % { New-Object IO.StreamReader($_) } | % { $_.ReadToEnd() }
}
手順⑤
以下を実行する。
curl.exe "https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/items" `
-H "Authorization: Bearer $idToken"
以下が表示されたら問題なく認証できています。
{"items":[]}
今後の予定
次回はフロント画面の構築を進めます!
Discussion