🔰

awsのコマンドでTODOリストを作ってみた

2023/10/28に公開

はじめに

最近awsを業務で使うことがあったので練習もかねてtodo listを作ってみました 参考になれば幸いです
(セキュリティなど不備あった教えてもらえると助かります)
今後、terraformで管理する構成も作れたらまとめていく予定です

注意点として、
・リセット周りの処理("delete-rest-api" etc...)もあります。すでに運用などしているアカウントで実行するとリセットされる可能性ありです
・コマンドのID周りについては、アカウントや実行時固有のものなので全て置き換える必要があります
具体的には、

  • AWS_ACCOUNT_ID: xxxxxxxxxxxx
  • aws_profile: pycetra
  • api_id
  • etc...
    (現在公開しているアプリでは、コマンドを再実行済みなので.html内のidなどはリセットされています)

作ったアプリについて

アプリ https://001-sample-app.s3.ap-northeast-1.amazonaws.com/index.html
・タスクの登録と削除だけできます
(未登録でアクセスするとリロードでデータが消えます)

ユーザー登録 https://001-sample-app.s3.ap-northeast-1.amazonaws.com/signup.html

コードの承認(ユーザー登録画面と同じ)

ログイン https://001-sample-app.s3.ap-northeast-1.amazonaws.com/signin.html

環境

  • AWS CLI
  • Lambda
  • DynamoDB
  • Cognito
  • API Gateway
  • JavaScript
  • chatGPT4(google検索と並行して使いました)

chatGPT4に質問することでこの記事にのっていることのほとんどを解決できました。その一方で一部回答の難しい部分があって、
・cmd028のcorsのjsonの記述ができませんでした。← googleで検索して解決
・そもそも今回のコマンドを一括出力ができない ← 全体の流れについて質問して、詳細な部分は別の質問に分けて解決
・"全体の流れについて質問して、詳細な部分は別の質問に分けて"がとても大変 ← 根気とやる気で解決
chatGPT4は月額3000円ほどかかりますが、初心者でもアプリ作れるところまでできるのでぜんぜんありかな〜と感じました

directory構成

.
├── frontend
│   ├── app.js            <- アプリ
│   ├── error.html
│   ├── index.html        <- アプリ
│   ├── signin.html       <- ログイン
│   ├── signup.html       <- ユーザー登録
│   └── styles.css
├── lambda
│   ├── dynamodb_reader
│   │   └── index.py      <- DynamoDBの読み込み
│   └── dynamodb_writer
│       └── index.py      <- DynamoDBの書き込み
├── test_01_cognito_signup_and_signin.py <- cognito認証のテスト
└── tools
    └── warrant.py

AWS CLIの実行ユーザーのロールについて

そもそもコマンドを実行するためにはIAMでポリシーを付与しておく必要があります
(実際の運用では大きすぎる権限かも?)

全ての実行コマンド(実行順)

1コマンド1秒待機して、apiをたたきすぎと怒られたり反映に時間がかかる部分はsleepコマンドをはさみました
参考までに、わたしの環境だと2分で最後まで実行できました

API GatewayとDynamoDBのリセットに関して

まず、古いapiやテーブルを削除することからスタートします。これは以前の設定やデータをクリアするためのステップです。

cmd001: AWS API GatewayでREST APIを一覧表示し、特定のREST APIを削除します。
cmd002: 新しいAPI GatewayのREST APIを作成します。
cmd003: DynamoDBのテーブルを削除します。
cmd004: DynamoDBのテーブルを新しく作成します。

cmd001

aws apigateway get-rest-apis \
    --profile pycetra
aws apigateway delete-rest-api \
    --rest-api-id 13tcxx0prj\
    --profile pycetra

cmd002

aws apigateway create-rest-api \
    --name 'MemoAppAPI' \
    --description 'API for Memo App' \
    --profile pycetra

cmd003

aws dynamodb delete-table \
    --table-name MemoTable \
    --profile pycetra

cmd004

sleep 2;
aws dynamodb create-table \
    --table-name MemoTable \
    --attribute-definitions AttributeName=UserId,AttributeType=S AttributeName=MemoId,AttributeType=S \
    --key-schema AttributeName=UserId,KeyType=HASH AttributeName=MemoId,KeyType=RANGE \
    --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
    --region ap-northeast-1  \
    --profile pycetra

API Gateway リソースの作成

cmd005: API Gatewayで新しいリソースを作成します。

cmd005

aws apigateway create-resource \
    --rest-api-id uetsnxntga \
    --parent-id lgo997cm6e \
    --path-part "memo" \
    --profile pycetra

IAM Roleのセットアップ

lambdaの実行権限を管理するためのroleをセットアップします。

cmd006: 既存のIAM Roleからポリシーを取り外し、そのRoleを削除します。
cmd007: 新しいIAM Roleを作成し、LambdaとDynamoDBに関するポリシーをアタッチします。

cmd006

aws iam detach-role-policy \
    --role-name LambdaExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess \
    --profile pycetra;
aws iam detach-role-policy \
    --role-name LambdaExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
    --profile pycetra;
aws iam delete-role \
    --role-name LambdaExecutionRole \
    --profile pycetra;

cmd007

aws iam create-role \
    --role-name LambdaExecutionRole \
    --assume-role-policy-document '{
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {"Service": "lambda.amazonaws.com"},
          "Action": "sts:AssumeRole"
        }
      ]
    }' \
    --profile pycetra;
aws iam attach-role-policy \
    --role-name LambdaExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess \
    --profile pycetra;
aws iam attach-role-policy \
    --role-name LambdaExecutionRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
    --profile pycetra;
sleep 10;

Lambdaのセットアップ

cmd008 - cmd017: ラムダ関数を準備、削除、作成、権限追加の一連のコマンドです。

cmd008

pip install PyJWT -t ./lambda/dynamodb_writer/lib/;
cd ./lambda/dynamodb_writer;
zip -r9 lambda_function.zip index.py lib/. 

cmd009

aws lambda delete-function \
    --function-name MemoAppFunction \
    --profile pycetra

cmd010

aws lambda create-function \
    --function-name MemoAppFunction \
    --runtime python3.8 \
    --role arn:aws:iam::xxxxxxxxxxxx:role/LambdaExecutionRole \
    --handler index.lambda_handler \
    --zip-file fileb://lambda/dynamodb_writer/lambda_function.zip \
    --profile pycetra

cmd011

aws lambda add-permission \
    --function-name MemoAppFunction \
    --statement-id apigateway-invoke \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn arn:aws:execute-api:ap-northeast-1:xxxxxxxxxxxx:uetsnxntga/*/POST/memo \
    --profile pycetra

cmd012

aws lambda get-policy --function-name MemoAppFunction --profile pycetra

cmd013

pip install PyJWT -t ./lambda/dynamodb_reader/lib/;
cd ./lambda/dynamodb_reader;
zip -r9 lambda_function.zip index.py lib/. 

cmd014

aws lambda delete-function \
    --function-name MemoReadFunction \
    --profile pycetra

cmd015

aws lambda create-function \
    --function-name MemoReadFunction \
    --runtime python3.8 \
    --role arn:aws:iam::xxxxxxxxxxxx:role/LambdaExecutionRole \
    --handler index.lambda_handler \
    --zip-file fileb://lambda/dynamodb_reader/lambda_function.zip \
    --profile pycetra

cmd016

aws lambda add-permission \
    --function-name MemoReadFunction \
    --statement-id apigateway-invoke \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn arn:aws:execute-api:ap-northeast-1:xxxxxxxxxxxx:uetsnxntga/*/GET/memo \
    --profile pycetra

cmd017

aws lambda get-policy --function-name MemoReadFunction --profile pycetra

API Gatewayのメソッド設定

cmd018 - cmd028: API GatewayにHTTPメソッドを設定し、Lambda関数との統合を行います。CORSに対応するための設定も含まれています。

cmd018

aws apigateway delete-method \
    --rest-api-id uetsnxntga \
    --resource-id arw3wt \
    --http-method POST \
    --profile pycetra

cmd019

aws apigateway put-method  \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method POST  \
    --authorization-type NONE  \
    --profile pycetra

cmd020

aws apigateway put-integration \
    --rest-api-id uetsnxntga \
    --resource-id arw3wt \
    --http-method POST \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:MemoAppFunction/invocations \
    --profile pycetra

test021

aws apigateway test-invoke-method \
			--rest-api-id uetsnxntga \
			--resource-id arw3wt \
		     --http-method POST \
		     --profile pycetra \
		     --body '{"memo": "テストの内容19"}'

cmd022

sleep 5;
aws apigateway create-deployment \
    --rest-api-id uetsnxntga \
    --stage-name prod \
    --profile pycetra

test023

sleep 1;
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"memo": "テストメモの内容10003"}' \
     https://uetsnxntga.execute-api.ap-northeast-1.amazonaws.com/prod/memo

cmd024

aws apigateway delete-method \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method OPTIONS \
    --profile pycetra

cmd025

aws apigateway put-method  \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method OPTIONS  \
    --authorization-type NONE  \
    --profile pycetra

cmd026

aws apigateway put-method-response \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method OPTIONS \
    --status-code 200 \
    --response-parameters "{ \
                    \"method.response.header.Access-Control-Allow-Headers\": false, \
                    \"method.response.header.Access-Control-Allow-Methods\": false, \
                    \"method.response.header.Access-Control-Allow-Origin\": false \
                }" \
    --response-models '{"application/json": "Empty"}' \
    --profile pycetra

cmd027

aws apigateway put-integration \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method OPTIONS \
    --type MOCK \
    --request-templates '{"application/json":"{\"statusCode\":200}"}' \
    --profile pycetra

cmd028

aws apigateway put-integration-response \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method OPTIONS \
    --status-code 200 \
    --response-parameters "{ \
                    \"method.response.header.Access-Control-Allow-Headers\": \"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'\", \
                    \"method.response.header.Access-Control-Allow-Methods\": \"'OPTIONS,POST,GET'\", \
                    \"method.response.header.Access-Control-Allow-Origin\": \"'*'\" \
                }"  \
    --profile pycetra

デプロイメントとテスト

cmd029 - cmd035: API Gatewayをデプロイし、エンドポイントの動作確認を行います。

cmd029

sleep 5;

cmd031

aws apigateway put-method  \
    --rest-api-id uetsnxntga  \
    --resource-id arw3wt  \
    --http-method GET  \
    --authorization-type NONE  \
    --profile pycetra

cmd032

aws apigateway put-integration \
    --rest-api-id uetsnxntga \
    --resource-id arw3wt \
    --http-method GET \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:MemoReadFunction/invocations \
    --profile pycetra

cmd033

aws apigateway test-invoke-method \
			--rest-api-id uetsnxntga \
			--resource-id arw3wt \
		     --http-method GET \
		     --profile pycetra 

cmd034

aws apigateway create-deployment \
    --rest-api-id uetsnxntga \
    --stage-name prod \
    --profile pycetra

cmd035

sleep 3;
curl -X GET \
     https://uetsnxntga.execute-api.ap-northeast-1.amazonaws.com/prod/memo

Cognitoのセットアップ

cmd036 - cmd039: Cognitoを利用してユーザー認証の機能をセットアップします。ユーザープールの削除、作成、クライアントの一覧表示、クライアントの作成などの手順が含まれています。

cmd036

aws cognito-idp list-user-pools \
    --max-results 60 \
    --profile pycetra
aws cognito-idp delete-user-pool \
    --user-pool-id ap-northeast-1_ML76AaOC7 \
    --profile pycetra

cmd037

aws cognito-idp create-user-pool \
  --pool-name SampleAppPool00001 \
  --auto-verified-attributes email \
  --username-attributes email \
  --policies '{
    "PasswordPolicy": {
      "MinimumLength": 8,
      "RequireUppercase": false,
      "RequireLowercase": false,
      "RequireNumbers": true,
      "RequireSymbols": true
    }
  }' \
  --schema '[
    {
      "Name": "email",
      "Required": true,
      "Mutable": true,
      "AttributeDataType": "String",
      "StringAttributeConstraints": {
        "MinLength": "0",
        "MaxLength": "2048"
      }
    },{
      "Name": "nickname",
      "Required": true,
      "Mutable": true,
      "AttributeDataType": "String",
      "StringAttributeConstraints": {
        "MinLength": "0",
        "MaxLength": "50"
      }
  },{ 
      "Name": "preferred_username", 
      "Required": true, 
      "Mutable": true, 
      "AttributeDataType": "String", 
      "StringAttributeConstraints": { 
        "MinLength": "0", 
        "MaxLength": "50" 
      } 
  } 
  ]' \
  --account-recovery-setting '{
    "RecoveryMechanisms": [
      {
        "Name": "verified_email",
        "Priority": 1
      }
    ]
  }' \
  --profile pycetra

cmd038

aws cognito-idp list-user-pool-clients \
    --user-pool-id ap-northeast-1_EqnBCX3Qo \
    --profile pycetra

cmd039

aws cognito-idp create-user-pool-client \
    --user-pool-id ap-northeast-1_EqnBCX3Qo \
    --client-name sample0001 \
    --profile pycetra

Lambdaの更新

cmd040~cmd043はlambda関数のアップデートをします。
というのも、ユーザー認証を追加するためにlambda関数も更新する必要があるためです。

cmd040: Python の PyJWT ライブラリを Lambda 用の dynamodb_writer ディレクトリにインストールし、その後で Lambda 関数の ZIP パッケージを作成します。
cmd041: dynamodb_writer の Lambda 関数のコードを更新します。
cmd042: Python の PyJWT ライブラリを Lambda 用の dynamodb_reader ディレクトリにインストールし、その後で Lambda 関数の ZIP パッケージを作成します。
cmd043: dynamodb_reader の Lambda 関数のコードを更新します。

cmd040

pip install PyJWT -t ./lambda/dynamodb_writer/lib/;
cd ./lambda/dynamodb_writer;
zip -r9 lambda_function.zip index.py lib/. ;

cmd041

aws lambda update-function-code \
    --function-name MemoAppFunction \
    --zip-file fileb://lambda/dynamodb_writer/lambda_function.zip \
    --profile pycetra

cmd042

pip install PyJWT -t ./lambda/dynamodb_reader/lib/;
cd ./lambda/dynamodb_reader;
zip -r9 lambda_function.zip index.py lib/. ;

cmd043

aws lambda update-function-code \
    --function-name MemoReadFunction \
    --zip-file fileb://lambda/dynamodb_reader/lambda_function.zip \
    --profile pycetra

S3に関する操作

cmd044: 指定されたS3バケットを削除します。
cmd045: 新しいS3バケットを作成します。
cmd046: ローカルのfrontendディレクトリの内容をS3バケットにコピーします。
cmd047: 公開アクセスのブロックをS3バケットから削除し、バケットポリシーを設定して、公開読み取りアクセスを許可します。

cmd044

aws s3 rb s3://001-sample-app --force --profile pycetra

cmd045

aws s3 mb s3://001-sample-app --profile pycetra

cmd046

aws s3 cp ./frontend s3://001-sample-app/ --recursive --profile pycetra

cmd047

aws s3api delete-public-access-block --bucket 001-sample-app --profile pycetra
aws s3api put-bucket-policy --bucket 001-sample-app --policy '{
		  "Version": "2012-10-17",
		  "Statement": [
		      {
		          "Sid": "PublicReadGetObject",
		          "Effect": "Allow",
		          "Principal": "*",
		          "Action": "s3:GetObject",
		          "Resource": "arn:aws:s3:::001-sample-app/*"
		      }
		  ]
		}' --profile pycetra

ユーザー認証のテスト

最後にユーザー登録やログインができることを確認しておきます。

python test_01_cognito_signup_and_signin.py

実行後に、新しいユーザー情報でDynamoDBに書き込みができていればOKです。

全ソースコード

frontend

./frontend/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>メモアプリ</title>
    <link rel="stylesheet" href="styles.css">
    <script src="https://cdn.amplify.aws/packages/core/5.0.5/aws-amplify-core.min.js" integrity="sha384-eM2urkpomL9SRm/kuPHZG3XPEItAiUAAyotT/AqlhSus8iAqs/EfHaYy1Jn5ih7K" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.amplify.aws/packages/auth/5.0.5/aws-amplify-auth.min.js" integrity="sha384-H25CFLYd7YHa1Oib73fs3kJN36VhaHHkLjo4AhGrhJ4HuKam05pg2/0t2MR6epun" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
    <div class="app">
        <h1>メモアプリ</h1>
        <textarea id="memoInput"></textarea>
        <button onclick="addMemo()">メモを追加</button>
        <ul id="memoList"></ul>
    </div>
    <script src="app.js"></script>
</body>
</html>

./frontend/app.js

const API_ENDPOINT = 'https://uetsnxntga.execute-api.ap-northeast-1.amazonaws.com/prod/memo'; // ここにAPI GatewayのエンドポイントURLを設定

let tasks = []; // タスクを保持する配列

function addMemo() {
    const memoInput = document.getElementById('memoInput');
    if (memoInput.value) {
        tasks.push(memoInput.value); // タスクを配列に追加
        updateMemoList(); // リストを更新
        saveMemoToDB() // DBに保存
            .catch(error => {
                console.error('Failed to save memo:', error);
            });
    }
}

async function saveMemoToDB() {
    const token = sessionStorage.getItem('idToken');
    const response = await fetch(API_ENDPOINT, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': token,
        },
        body: JSON.stringify({ memo: tasks }) // tasksを文字列化して送信
    });

    if (!response.ok) {
        throw new Error('Failed to save memo to DB');
    }

    return response.json();
}

// リストの表示を更新する関数
function updateMemoList() {
    const memoList = document.getElementById('memoList');
    memoList.innerHTML = ''; // 現在のリストをクリア
    tasks.forEach((task, index) => {
        const listItem = document.createElement('li');
        listItem.textContent = task;
        
        // 削除ボタンを追加
        const deleteButton = document.createElement('button');
        deleteButton.textContent = 'Delete';
        deleteButton.onclick = () => {
            tasks.splice(index, 1); // タスクを削除
            updateMemoList(); // リストを更新
            saveMemoToDB().catch(error => { // DBを更新
                console.error('Failed to delete memo:', error);
            });
        };

        listItem.appendChild(deleteButton);
        memoList.appendChild(listItem);
    });
}

document.addEventListener('DOMContentLoaded', (event) => {
    loadMemosFromDB().catch(error => {
        console.error('Failed to load memos:', error);
    });
});

async function loadMemosFromDB() {
    const token = sessionStorage.getItem('idToken');
    const response = await fetch(API_ENDPOINT, {
        method: 'GET',
        headers: {
            'Authorization': token,
        },
    });

    if (!response.ok) {
        throw new Error('Failed to load memos from DB');
    }

    const data = await response.json();
    tasks_tmp = data[0].Content 
    if (tasks_tmp === null){
        tasks = []
    } else if (typeof tasks_tmp === "string"){
        tasks = [tasks_tmp]
    } else if (Array.isArray(tasks_tmp)){
        tasks = tasks_tmp
    }

    updateMemoList(); // リストを更新
}

./frontend/error.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>エラーページ</title>
<style>
  body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    color: #333;
    margin: 0;
    padding: 0;
  }
  .container {
    max-width: 600px;
    margin: 50px auto;
    background-color: #fff;
    padding: 30px;
    border-radius: 5px;
    box-shadow: 0px 0px 10px 0px #000;
  }
  h1 {
    color: #dc3545;
    margin-bottom: 30px;
  }
</style>
</head>
<body>

<div class="container">
  <h1>エラーが発生しました</h1>
  <p>申し訳ありませんが、何らかのエラーが発生しました。少し経ってからもう一度試してください。</p>
  <button id="retryButton">再試行</button>
</div>

<script>
  document.getElementById("retryButton").onclick = function() {
    location.reload();
  };
</script>

</body>
</html>

./frontend/signin.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Singin</title>
        <script src="https://cdn.amplify.aws/packages/core/5.0.5/aws-amplify-core.min.js" integrity="sha384-eM2urkpomL9SRm/kuPHZG3XPEItAiUAAyotT/AqlhSus8iAqs/EfHaYy1Jn5ih7K" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
        <script src="https://cdn.amplify.aws/packages/auth/5.0.5/aws-amplify-auth.min.js" integrity="sha384-H25CFLYd7YHa1Oib73fs3kJN36VhaHHkLjo4AhGrhJ4HuKam05pg2/0t2MR6epun" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    </head>
    <body>

        <h2>SignIn</h2>
        <div>
            email: <input id="signin-email" type="text">
            Password: <input id="signin-password" type="password">
            <button onclick="signIn()">Sign In</button>
        </div>

        <script>
            const { Amplify } = aws_amplify_core;
            Amplify.configure({
                Auth: {
                    region: 'ap-northeast-1',
                    userPoolId: 'ap-northeast-1_EqnBCX3Qo',
                    userPoolWebClientId: '5vk6q9c7j1nj3nfva2eijfkmu4',
                }
            });

            // Sign in function
            async function signIn() {
                const email = document.getElementById('signin-email').value; 
                const password = document.getElementById('signin-password').value;
                try {
                    const user = await Amplify.Auth.signIn(email, password);
                    console.log('Sign in success:', user);

                    sessionStorage.setItem('idToken', user.signInUserSession.idToken.jwtToken);
                    window.location.href = '../index.html';
                } catch (error) {
                    console.error('Sign in error:', error);
                }
            }
        </script>
    </body>
</html>

./frontend/signup.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Signup</title>
        <script src="https://cdn.amplify.aws/packages/core/5.0.5/aws-amplify-core.min.js" integrity="sha384-eM2urkpomL9SRm/kuPHZG3XPEItAiUAAyotT/AqlhSus8iAqs/EfHaYy1Jn5ih7K" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
        <script src="https://cdn.amplify.aws/packages/auth/5.0.5/aws-amplify-auth.min.js" integrity="sha384-H25CFLYd7YHa1Oib73fs3kJN36VhaHHkLjo4AhGrhJ4HuKam05pg2/0t2MR6epun" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    </head>
    <body>
        <!-- SignUpセクション -->
        <div id="signup-section">
            <h2>SignUp</h2>
            <div>
                DisplayName: <input id="signup-nickname" type="text">
                Email: <input id="signup-email" type="text">
                Password: <input id="signup-password" type="password">
                <button onclick="signUp()">Sign Up</button>
            </div>
        </div>


        <!-- Confirmセクション -->
        <div id="confirm-section" style="display: none;">
            <h2>Confirm SignUp</h2>
            <div>
                Email: <input id="confirm-email" type="text">
                Code: <input id="confirm-code" type="text">
                <button onclick="confirmSignUp()">Confirm</button>
            </div>
        </div>

        <p>Already have an account? <a href="signin.html">Sign In</a></p>

        <script>
            const { Amplify } = aws_amplify_core;
            Amplify.configure({
                Auth: {
                    region: 'ap-northeast-1',
                    userPoolId: 'ap-northeast-1_EqnBCX3Qo',
                    userPoolWebClientId: '5vk6q9c7j1nj3nfva2eijfkmu4',
                }
            });

            async function signUp() {
                const nickname = document.getElementById('signup-nickname').value;
                const email = document.getElementById('signup-email').value;
                const password = document.getElementById('signup-password').value;
                try {
                    await Amplify.Auth.signUp({
                        username: email,
                        password,
                        attributes: {
                            nickname,
                            preferred_username: nickname
                        }
                    });
                    console.log('Sign up success');

                    document.getElementById('signup-section').style.display = 'none';
                    document.getElementById('confirm-section').style.display = 'block';
                    document.getElementById('confirm-email').value = email;
                } catch (error) {
                    console.error('Sign up error:', error);
                }
            }

            // Confirm sign up function
            async function confirmSignUp() {
                const email = document.getElementById('signup-email').value;
                const password = document.getElementById('signup-password').value;
                const code = document.getElementById('confirm-code').value;
                try {
                    const result = await Amplify.Auth.confirmSignUp(email, code);
                    console.log('Confirmation success:', result);
                    try {
                        const user = await Amplify.Auth.signIn(email, password);
                        console.log('Sign in success:', user);
                        sessionStorage.setItem('idToken', user.signInUserSession.idToken.jwtToken);
                        window.location.href = '../index.html';
                    } catch (error) {
                        console.error('Sign in error:', error);
                    }
                } catch (error) {
                    console.error('Confirmation error:', error);
                }
            }

        </script>
    </body>
</html>

./frontend/styles.css

.app {
    font-family: Arial, sans-serif;
    width: 300px;
    margin: 0 auto;
    padding-top: 50px;
}

textarea {
    width: 100%;
    height: 100px;
}

button {
    display: block;
    width: 100%;
    padding: 10px;
    margin-top: 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    cursor: pointer;
}

./lambda/dynamodb_reader/index.py

import json

import boto3
from lib import authconfig, jwt

dynamo = boto3.resource("dynamodb")
table = dynamo.Table("MemoTable")


def lambda_handler(event, context):
    # Authorizationの確認とユーザーIDの取得
    if authconfig.AUTH_FLAG:
        if "headers" in event and "Authorization" in event["headers"]:
            token = event["headers"]["Authorization"]

            # トークンをデコードしてユーザーIDを取得
            decoded_payload = jwt.decode(
                token, algorithms=["RS256"], options={"verify_signature": False}
            )
            user_id = decoded_payload["email"]
        else:
            return {
                "statusCode": 401,
                "headers": {
                    "Access-Control-Allow-Origin": "*",
                    "Access-Control-Allow-Credentials": True,
                },
                "body": json.dumps({"error": "Authorization not found"}),
            }
    else:
        user_id = "aaa_from_curl"  # テスト用のユーザーID

    # DynamoDBからデータを取得
    try:
        response = table.query(
            KeyConditionExpression=boto3.dynamodb.conditions.Key("UserId").eq(user_id)
        )
    except Exception as e:
        print(e)
        return {
            "statusCode": 500,
            "headers": {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Credentials": True,
            },
            "body": json.dumps({"error": "Failed to retrieve memos."}),
        }

    # 結果を返す
    return {
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Credentials": True,
        },
        "body": json.dumps(response["Items"]),
    }

Lambda

./lambda/dynamodb_reader/lib/authconfig.py

AUTH_FLAG = False

↑cognito作成前
↓cognito作成後

AUTH_FLAG = True

./lambda/dynamodb_writer/index.py

import json
import time

import boto3
from lib import authconfig, jwt

dynamo = boto3.resource("dynamodb")
table = dynamo.Table("MemoTable")


def lambda_handler(event, context):
    if authconfig.AUTH_FLAG:
        # JWTトークンを取得
        if "headers" in event and "Authorization" in event["headers"]:
            token = event["headers"]["Authorization"]

            # トークンをデコードしてユーザーIDを取得
            decoded_payload = jwt.decode(
                token, algorithms=["RS256"], options={"verify_signature": False}
            )
            user_id = decoded_payload["email"]
        else:
            token = ""
            print("Headers or Authorization not found in event")
    else:
        user_id = "aaa_from_curl"  # curlコマンドのテスト用

    if event["body"]:
        body = json.loads(event["body"])
    else:
        body = {}
    memo = body["memo"]

    key = {"UserId": user_id, "MemoId": "00001.md"}

    update_expression = "set Content = :c"
    expression_attribute_values = {":c": memo}

    try:
        table.update_item(
            Key=key,
            UpdateExpression=update_expression,
            ExpressionAttributeValues=expression_attribute_values,
            ReturnValues="UPDATED_NEW",
        )
        return {
            "statusCode": 200,
            "headers": {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Credentials": True,
            },
            "body": json.dumps({"message": "Memo created successfully."}),
        }
    except Exception as e:
        print(e)
        return {
            "statusCode": 500,
            "headers": {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Credentials": True,
            },
            "body": json.dumps({"error": "Failed to upsert memo."}),
        }

./lambda/dynamodb_writer/lib/authconfig.py

AUTH_FLAG = False

↑cognito作成前
↓cognito作成後

AUTH_FLAG = True

Cognito

./test_01_cognito_signup_and_signin.py

import datetime
import json

import boto3
import requests

from tools import utils, warrant

session = boto3.Session(profile_name=utils.CONFIG["aws_profile"])
client_admin = session.client("cognito-idp", region_name="ap-northeast-1")


client = boto3.client("cognito-idp", region_name="ap-northeast-1")


nickname = "aaaa"


def signup_by_admin(user_pool_id, username, password):
    client_admin.admin_create_user(
        UserPoolId=user_pool_id,
        Username=username,
        TemporaryPassword=password,
        UserAttributes=[
            {"Name": "nickname", "Value": nickname},
            {"Name": "preferred_username", "Value": nickname},
        ],
        MessageAction="SUPPRESS",
    )
    client_admin.admin_set_user_password(
        UserPoolId=user_pool_id, Username=username, Password=password, Permanent=True
    )


def signup_by_user(user_pool_id, client_id, username, password):
    # この関数はユーザーにverify code付きのメールが送信される
    client.sign_up(
        ClientId=client_id,
        Username=username,
        Password=password,
        UserAttributes=[
            {"Name": "nickname", "Value": nickname},
            {"Name": "preferred_username", "Value": nickname},
        ],
    )

    # 管理者による承認(verify codeでユーザーに本来は承認プロセスの一部を担ってもらうが、未承認だとエラーがおきるため自動化する)
    client_admin.admin_confirm_sign_up(UserPoolId=user_pool_id, Username=username)


def signin(user_pool_id, client_id, username, password):
    u = warrant.AWSSRP(
        pool_id=user_pool_id,
        client_id=client_id,
        username=username,
        password=password,
        pool_region="ap-northeast-1",
    )
    response = u.authenticate_user()
    return response


def saveTextData(response, api_id):
    api_endpoint = (
        "https://" + api_id + ".execute-api.ap-northeast-1.amazonaws.com/prod/memo"
    )
    id_token = response["AuthenticationResult"]["IdToken"]
    memo = "abcdefg"

    headers = {"Content-Type": "application/json", "Authorization": id_token}

    data = {"memo": memo}

    response = requests.post(api_endpoint, headers=headers, data=json.dumps(data))

    print(response.status_code)
    print(response.text)


def test_01_admin(user_pool_id, client_id, username, password, api_id):
    signup_by_admin(user_pool_id, username, password)
    response = signin(user_pool_id, client_id, username, password)
    saveTextData(response, api_id)


def test_02_user(user_pool_id, client_id, username, password, api_id):
    signup_by_user(user_pool_id, client_id, username, password)
    response = signin(user_pool_id, client_id, username, password)
    saveTextData(response, api_id)


def readTextData(response, api_id):
    api_endpoint = (
        "https://" + api_id + ".execute-api.ap-northeast-1.amazonaws.com/prod/memo"
    )
    id_token = response["AuthenticationResult"]["IdToken"] 
    headers = {"Content-Type": "application/json", "Authorization": id_token}

    response = requests.get(api_endpoint, headers=headers)

    print(response.status_code)
    print(response.text)


def test_03_user_read(user_pool_id, client_id, username, password, api_id):
    response = signin(user_pool_id, client_id, username, password)
    readTextData(response, api_id)


if __name__ == "__main__":
    user_pool_id = ""
    user_pool_client_id = ""
    api_id = ""

    prefix = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    username_case1 = prefix + "case01" + "@abc.co.jp"
    username_case2 = prefix + "case02" + "@abc.co.jp"
    password = "YcGQD5DLM9YKyJn_"

    test_01_admin(
        user_pool_id=user_pool_id,
        client_id=user_pool_client_id,
        username=username_case1,
        password=password,
        api_id=api_id,
    )

    test_02_user(
        user_pool_id=user_pool_id,
        client_id=user_pool_client_id,
        username=username_case2,
        password=password,
        api_id=api_id,
    )

    test_03_user_read(
        user_pool_id=user_pool_id,
        client_id=user_pool_client_id,
        username=username_case1,
        password=password,
        api_id=api_id,
    )

./tools/warrant.py

############################################
# copy して作成(参考 https://github.com/capless/warrant/blob/master/warrant/aws_srp.py )
# というのも、pip install warrantの後に、importするとpython 3.9だとエラーが発生したから
############################################


import base64
import binascii
import datetime
import hashlib
import hmac
import os
import re

import boto3
import six

# https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22
n_hex = (
    "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
    + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
    + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
    + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
    + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
    + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
    + "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
    + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
    + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
    + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
    + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
    + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
    + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
    + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
    + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
    + "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"
)
# https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L49
g_hex = "2"
info_bits = bytearray("Caldera Derived Key", "utf-8")


def hash_sha256(buf):
    """AuthenticationHelper.hash"""
    a = hashlib.sha256(buf).hexdigest()
    return (64 - len(a)) * "0" + a


def hex_hash(hex_string):
    return hash_sha256(bytearray.fromhex(hex_string))


def hex_to_long(hex_string):
    return int(hex_string, 16)


def long_to_hex(long_num):
    return "%x" % long_num


def get_random(nbytes):
    random_hex = binascii.hexlify(os.urandom(nbytes))
    return hex_to_long(random_hex)


def pad_hex(long_int):
    """
    Converts a Long integer (or hex string) to hex format padded with zeroes for hashing
    :param {Long integer|String} long_int Number or string to pad.
    :return {String} Padded hex string.
    """
    if not isinstance(long_int, six.string_types):
        hash_str = long_to_hex(long_int)
    else:
        hash_str = long_int
    if len(hash_str) % 2 == 1:
        hash_str = "0%s" % hash_str
    elif hash_str[0] in "89ABCDEFabcdef":
        hash_str = "00%s" % hash_str
    return hash_str


def compute_hkdf(ikm, salt):
    """
    Standard hkdf algorithm
    :param {Buffer} ikm Input key material.
    :param {Buffer} salt Salt value.
    :return {Buffer} Strong key material.
    @private
    """
    prk = hmac.new(salt, ikm, hashlib.sha256).digest()
    info_bits_update = info_bits + bytearray(chr(1), "utf-8")
    hmac_hash = hmac.new(prk, info_bits_update, hashlib.sha256).digest()
    return hmac_hash[:16]


def calculate_u(big_a, big_b):
    """
    Calculate the client's value U which is the hash of A and B
    :param {Long integer} big_a Large A value.
    :param {Long integer} big_b Server B value.
    :return {Long integer} Computed U value.
    """
    u_hex_hash = hex_hash(pad_hex(big_a) + pad_hex(big_b))
    return hex_to_long(u_hex_hash)


class AWSSRP(object):
    NEW_PASSWORD_REQUIRED_CHALLENGE = "NEW_PASSWORD_REQUIRED"
    PASSWORD_VERIFIER_CHALLENGE = "PASSWORD_VERIFIER"

    def __init__(
        self,
        username,
        password,
        pool_id,
        client_id,
        pool_region=None,
        client=None,
        client_secret=None,
    ):
        if pool_region is not None and client is not None:
            raise ValueError(
                "pool_region and client should not both be specified "
                "(region should be passed to the boto3 client instead)"
            )

        self.username = username
        self.password = password
        self.pool_id = pool_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.client = (
            client if client else boto3.client("cognito-idp", region_name=pool_region)
        )
        self.big_n = hex_to_long(n_hex)
        self.g = hex_to_long(g_hex)
        self.k = hex_to_long(hex_hash("00" + n_hex + "0" + g_hex))
        self.small_a_value = self.generate_random_small_a()
        self.large_a_value = self.calculate_a()

    def generate_random_small_a(self):
        """
        helper function to generate a random big integer
        :return {Long integer} a random value.
        """
        random_long_int = get_random(128)
        return random_long_int % self.big_n

    def calculate_a(self):
        """
        Calculate the client's public value A = g^a%N
        with the generated random number a
        :param {Long integer} a Randomly generated small A.
        :return {Long integer} Computed large A.
        """
        big_a = pow(self.g, self.small_a_value, self.big_n)
        # safety check
        if (big_a % self.big_n) == 0:
            raise ValueError("Safety check for A failed")
        return big_a

    def get_password_authentication_key(self, username, password, server_b_value, salt):
        """
        Calculates the final hkdf based on computed S value, and computed U value and the key
        :param {String} username Username.
        :param {String} password Password.
        :param {Long integer} server_b_value Server B value.
        :param {Long integer} salt Generated salt.
        :return {Buffer} Computed HKDF value.
        """
        u_value = calculate_u(self.large_a_value, server_b_value)
        if u_value == 0:
            raise ValueError("U cannot be zero.")
        username_password = "%s%s:%s" % (self.pool_id.split("_")[1], username, password)
        username_password_hash = hash_sha256(username_password.encode("utf-8"))

        x_value = hex_to_long(hex_hash(pad_hex(salt) + username_password_hash))
        g_mod_pow_xn = pow(self.g, x_value, self.big_n)
        int_value2 = server_b_value - self.k * g_mod_pow_xn
        s_value = pow(int_value2, self.small_a_value + u_value * x_value, self.big_n)
        hkdf = compute_hkdf(
            bytearray.fromhex(pad_hex(s_value)),
            bytearray.fromhex(pad_hex(long_to_hex(u_value))),
        )
        return hkdf

    def get_auth_params(self):
        auth_params = {
            "USERNAME": self.username,
            "SRP_A": long_to_hex(self.large_a_value),
        }
        if self.client_secret is not None:
            auth_params.update(
                {
                    "SECRET_HASH": self.get_secret_hash(
                        self.username, self.client_id, self.client_secret
                    )
                }
            )
        return auth_params

    @staticmethod
    def get_secret_hash(username, client_id, client_secret):
        message = bytearray(username + client_id, "utf-8")
        hmac_obj = hmac.new(bytearray(client_secret, "utf-8"), message, hashlib.sha256)
        return base64.standard_b64encode(hmac_obj.digest()).decode("utf-8")

    def process_challenge(self, challenge_parameters):
        user_id_for_srp = challenge_parameters["USER_ID_FOR_SRP"]
        salt_hex = challenge_parameters["SALT"]
        srp_b_hex = challenge_parameters["SRP_B"]
        secret_block_b64 = challenge_parameters["SECRET_BLOCK"]
        # re strips leading zero from a day number (required by AWS Cognito)
        timestamp = re.sub(
            r" 0(\d) ",
            r" \1 ",
            datetime.datetime.utcnow().strftime("%a %b %d %H:%M:%S UTC %Y"),
        )
        hkdf = self.get_password_authentication_key(
            user_id_for_srp, self.password, hex_to_long(srp_b_hex), salt_hex
        )
        secret_block_bytes = base64.standard_b64decode(secret_block_b64)
        msg = (
            bytearray(self.pool_id.split("_")[1], "utf-8")
            + bytearray(user_id_for_srp, "utf-8")
            + bytearray(secret_block_bytes)
            + bytearray(timestamp, "utf-8")
        )
        hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256)
        signature_string = base64.standard_b64encode(hmac_obj.digest())
        response = {
            "TIMESTAMP": timestamp,
            "USERNAME": user_id_for_srp,
            "PASSWORD_CLAIM_SECRET_BLOCK": secret_block_b64,
            "PASSWORD_CLAIM_SIGNATURE": signature_string.decode("utf-8"),
        }
        if self.client_secret is not None:
            response.update(
                {
                    "SECRET_HASH": self.get_secret_hash(
                        self.username, self.client_id, self.client_secret
                    )
                }
            )
        return response

    def authenticate_user(self, client=None):
        boto_client = self.client or client
        auth_params = self.get_auth_params()
        response = boto_client.initiate_auth(
            AuthFlow="USER_SRP_AUTH",
            AuthParameters=auth_params,
            ClientId=self.client_id,
        )
        if response["ChallengeName"] == self.PASSWORD_VERIFIER_CHALLENGE:
            challenge_response = self.process_challenge(response["ChallengeParameters"])
            tokens = boto_client.respond_to_auth_challenge(
                ClientId=self.client_id,
                ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
                ChallengeResponses=challenge_response,
            )

            if tokens.get("ChallengeName") == self.NEW_PASSWORD_REQUIRED_CHALLENGE:
                raise ForceChangePasswordException(
                    "Change password before authenticating"
                )

            return tokens
        else:
            raise NotImplementedError(
                "The %s challenge is not supported" % response["ChallengeName"]
            )

    def set_new_password_challenge(self, new_password, client=None):
        boto_client = self.client or client
        auth_params = self.get_auth_params()
        response = boto_client.initiate_auth(
            AuthFlow="USER_SRP_AUTH",
            AuthParameters=auth_params,
            ClientId=self.client_id,
        )
        if response["ChallengeName"] == self.PASSWORD_VERIFIER_CHALLENGE:
            challenge_response = self.process_challenge(response["ChallengeParameters"])
            tokens = boto_client.respond_to_auth_challenge(
                ClientId=self.client_id,
                ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
                ChallengeResponses=challenge_response,
            )

            if tokens["ChallengeName"] == self.NEW_PASSWORD_REQUIRED_CHALLENGE:
                challenge_response = {
                    "USERNAME": auth_params["USERNAME"],
                    "NEW_PASSWORD": new_password,
                }
                new_password_response = boto_client.respond_to_auth_challenge(
                    ClientId=self.client_id,
                    ChallengeName=self.NEW_PASSWORD_REQUIRED_CHALLENGE,
                    Session=tokens["Session"],
                    ChallengeResponses=challenge_response,
                )
                return new_password_response
            return tokens
        else:
            raise NotImplementedError(
                "The %s challenge is not supported" % response["ChallengeName"]
            )

GitHubで編集を提案

Discussion