awsのコマンドでTODOリストを作ってみた
はじめに
最近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"]
)
Discussion