🛒

【AWS×Java】SAMで冷蔵庫管理アプリを構築 #3 Cognito編

に公開

はじめに

当記事の最終ゴールについては以下の記事をご確認ください。
https://zenn.dev/superrelax102/articles/424ebd380d6c53

今回のゴール:Cognito User Poolでログインできる
API Gatewayを User Pool Authorizerで保護できる
フロント対応に向けたCORS対応ができる
前提読者:AWS初心者、開発初心者

また、今回は以下「【AWS×Java】SAMで冷蔵庫管理アプリを構築 #2 DynamoDB編(CRUD API②)」を実施済みの前提で進めます。
https://zenn.dev/superrelax102/articles/386db0ea9fcff2

以下を実施することで従量課金が発生します。その点については自己責任でお願いします。無料枠があるが上限があります。削除することで課金を止めることが可能です。
また、本記事は学習ログです。詳細は公式ドキュメントを適宜参照してください。

アーキテクチャ概要

今回は以下赤枠部分を完成させます。

図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ファイルに設定するため、以下の修正を行います。

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必須になります。

template.yaml(関連部分のみ抜粋)
Globals:
  Function:
    Timeout: 20
    MemorySize: 512
+  Api:
+    Auth:
+      Authorizers:
+        CognitoAuthorizer:
+          UserPoolArn: !GetAtt FridgeUserPool.Arn # 既存のUserPool
+      DefaultAuthorizer: CognitoAuthorizer

③フロント実装に向けたCORS対応

私はこのアプリを一度完成させておりませんが、一番困ったのはCORSかなと思います。私は理解せぬまま進めたのでかなり時間がかかりましたが、一度しっかりと内容を理解したほうがいいかと思います。
以下、AWSサイトのCORSに関する説明をご確認ください。

https://aws.amazon.com/jp/what-is/cross-origin-resource-sharing/

上記記載の通り、curlコマンド等でAPIの動作を確認する上では考慮不要なのですが、次回のフロントから今回作成したAPIを使用する際にCORSの考慮が必要です。

以下は平たく言うと以下2つの修正をしています。
 ①フロントのURL「http://localhost:5173」からのアクセスのみ許可する。
 ②OPTIONSに認証を掛けない(プリフライトは無認可でOKを返す)

template.yaml(関連部分のみ抜粋)
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 # プリフライトの設定
App.java(関連部分のみ抜粋)
    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