17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React/Go】ユーザー認証をCognito + Amplifyで構築してみた ~ユーザ登録/削除編~

Posted at

はじめに

Reactで作成したWebアプリケーションのユーザー認証部分をCognito + Amplifyフレームワークで構築してみました。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の構築準備編構築完成編をご覧ください。
本記事は、アプリケーションからユーザーを登録、削除する方法についてまとめています。

完成画面

今回は、アプリケーションからユーザーを登録すると、ユーザープールとDBそれぞれにユーザーが登録されて、画面には「ユーザーを登録しました。」というアラートが出力されるようにします。

スクリーンショット 2020-12-10 19.15.23.png

[Submit]をクリックすると↓↓↓

画面
スクリーンショット 2020-12-10 19.15.11.png

Cognitoユーザープール管理画面
スクリーンショット 2020-12-06 18.55.55.png

DB(userテーブル)※一部抜粋

+----+-----------+------------------+
| id | user_name | email            |
+----+-----------+------------------+
|  2 | test      | test@example.com |
+----+-----------+------------------+

方法検討

要件

構築方法を考えるにあたり、条件は以下の通りです。

  • 静的コンテンツをS3に置いている
  • アプリケーション部分はLambda + RDS Proxy + RDSで実装している
  • ユーザーデータはCognitoユーザープール以外に、RDSに保存している
  • NATゲートウェイはコストが高いので使いたくない

現在の構成図(ユーザー認証付加前)

ユーザー認証付加前のアプリケーション部分の構成図は下記の通りです。

Cognito-before.png

VPC Lambdaによる弊害

ここで、LambdaをVPC内に設置していることで、Cognitoにアクセスできないことに気付きました。パブリックサブネットに置いているんだから、アクセスできると勝手に思っていました。

AWS開発者ガイドによると、次のように説明されています。

プライベートリソースにアクセスするには、関数をプライベートサブネットに接続します。関数にインターネットアクセスが必要な場合は、ネットワークアドレス変換 (NAT) を使用します。関数をパブリックサブネットに接続しても、インターネットアクセスやパブリック IP アドレスは提供されません。

Lambda関数をパブリックサブネットに接続しても、インターネットアクセスやパブリック IP アドレスは提供されないんです。NATゲートウェイを使用する場合にもLambda関数はプライベートサブネットに置くべきだそうです。パブリックサブネットにLambdaを置いておくメリットはなさそうなので、VPC Lambdaはプライベートサブネットに置きましょう!!!

結論

この条件に沿ってアプリケーションの登録、削除処理を考えた結果、VPC Lambdaをプライベートサブネットに移動させ、NATゲートウェイは使いたくないので、強引にLambdaからLambdaを呼び出すことにしました。

シーケンス図

シーケンス図を書くと次のようになります。

スクリーンショット 2020-12-15 20.00.08.png

構成図(ユーザー認証付加後)

構成は下図の通りになりました。

Cognito-after (1).png

手順

下記の流れで進めていきます。

  1. RDSを更新するLambda関数:Lambda(VPC) の作成
  2. Cognitoを更新するLambda関数:Lambda(非VPC) の作成
  3. API Gatewayの作成
  4. フロントの実装

ユーザーを登録する

1. DBを更新するLambda関数:Lambda(VPC) の作成

Lambda(非VPC)の作成時につけるIAMロールにLambda(VPC)のarnが必要なので、先にLambda(VPC)から作成します。
VPC内に設置してRDSに情報を書き込むLambdaを作成していきます。このLambdaに関しては、RDSにデータが保存できれば良く、特に既存のLambdaと変わりないので割愛します。
祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみたの「8. Lambda関数の作成」を参考に作成しました。

ソースコード

ソースコードはこのような感じです。
※↓クリックするとソースコードが見れます。

ソースコード
lambda_vpc.go

package main

import (
	"database/sql"
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
	_ "github.com/go-sql-driver/mysql"
	"os"
)

type MyEvent struct {
	UserName string `json:"userName"`
	Email    string `json:"email"`
}

// os.Getenv()でLambdaの環境変数を取得
var dbEndpoint = os.Getenv("dbEndpoint")
var dbUser = os.Getenv("dbUser")
var dbPass = os.Getenv("dbPass")
var dbName = os.Getenv("dbName")

func RDSConnect() (*sql.DB, error) {
	connectStr := fmt.Sprintf(
		"%s:%s@tcp(%s:%s)/%s?charset=%s",
		dbUser,
		dbPass,
		dbEndpoint,
		"3306",
		dbName,
		"utf8",
	)
	db, err := sql.Open("mysql", connectStr)
	if err != nil {
		return nil, err
	}
	return db, nil
}

func RDSProcessing(event MyEvent, db *sql.DB) (interface{}, error) {

	tx, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	// ユーザーテーブルに情報を登録
	stmt, err := tx.Prepare("INSERT INTO user(user_name, email) VALUES (?, ?) ")
	if err != nil {
		return nil, err
	}
	defer stmt.Close()

	if _, err := stmt.Exec(event.UserName, event.Email); err != nil {
		return nil, err
	}

	if err := tx.Commit(); err != nil {
		return nil, err
	}

	response := "正常に処理が完了しました。"
	return response, nil
}

func run(event MyEvent) (interface{}, error) {
	fmt.Println("RDS接続 start!")
	db, err := RDSConnect()
	if err != nil {
		fmt.Println("DBの接続に失敗しました。")
		panic(err.Error())
	}
	fmt.Println("RDS接続 end!")
	fmt.Println("RDS処理 start!")
	response, err := RDSProcessing(event, db)
	if err != nil {
		fmt.Println("DB処理に失敗しました。")
		panic(err.Error())
	}
	fmt.Println("RDS処理 end!")
	return response, nil
}

/**************************
   メイン
**************************/
func main() {
	lambda.Start(run)
}

2. Cognitoを更新するLambda関数:Lambda(非VPC) の作成

VPCの外に置いて、Cognitoユーザープールへの登録とRDSを更新するLambda(VPC)を実行するLambdaを作成していきます。

IAMロール

下記の2つの権限をつけたポリシーを作成してアタッチします。

  • Cognitoユーザープールにユーザーを登録/削除する権限
  • Lambda(VPC)を実行する権限
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cognito-idp:AdminDeleteUser",
                "cognito-idp:AdminCreateUser"
            ],
            "Resource": "<Cognitoのarn>"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "<Lambda(VPC)のarn>"
        }
    ]
}

ソースコード

lambda_no_vpc.go

package main

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
	"github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
	l "github.com/aws/aws-sdk-go/service/lambda"
	"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
)

type MyEvent struct {
	UserName string `json:"userName"`
	Email    string `json:"email"`
}

func AddCognitoUser(svc cognitoidentityprovideriface.CognitoIdentityProviderAPI, event MyEvent) error {

	// 登録時にユーザーにメール送信
	desiredDeliveryMediums := []*string{aws.String("EMAIL")}

	// メールアドレスとメールアドレス検証済みを設定
	userAttributes := []*cognitoidentityprovider.AttributeType{
		{
			Name:  aws.String("email"),
			Value: aws.String(event.Email),
		},
		{
			Name:  aws.String("email_verified"),
			Value: aws.String("true"),
		},
	}

	// ユーザープールの設定
	// os.Getenv()でLambdaの環境変数を取得
	userPoolId := aws.String(os.Getenv("userPoolId"))

	// ユーザー名の設定
	username := aws.String(event.UserName)

	// Inputの作成
	input := &cognitoidentityprovider.AdminCreateUserInput{}
	input.DesiredDeliveryMediums = desiredDeliveryMediums
	input.UserAttributes = userAttributes
	input.UserPoolId = userPoolId
	input.Username = username

	// 処理実行
	result, err := svc.AdminCreateUser(input)
	if err != nil {
		fmt.Println(err.Error())
		return err
	}

	fmt.Println(result)
	return nil
}

func AddDbUser(svc lambdaiface.LambdaAPI, event MyEvent) error {

	// ValidateLambdaに送る情報の作成
	jsonBytes, _ := json.Marshal(event)

	// Inputの作成
	input := &l.InvokeInput{}
	input.FunctionName = aws.String(os.Getenv("arn"))
	input.Payload = jsonBytes
	input.InvocationType = aws.String("RequestResponse") // 同期実行

	// 処理実行
	result, err := svc.Invoke(input)
	if err != nil {
		fmt.Println(err.Error())
		return err
	}

	fmt.Println(result)
	fmt.Println(string(result.Payload))

	return nil
}

func run(event MyEvent) (interface{}, error) {
	fmt.Println("Cognito登録 start!")
	// セッション作成
	csvc := cognitoidentityprovider.New(session.Must(session.NewSession()))

	if err := AddCognitoUser(csvc, event); err != nil {
		fmt.Println("ユーザー登録に失敗しました。")
		panic(err.Error())
	}
	fmt.Println("Cognito登録 end!")

	fmt.Println("db登録 start!")
	// セッションの作成
	lsvc := l.New(session.Must(session.NewSession()))

	if err := AddDbUser(lsvc, event); err != nil {
		fmt.Println("ユーザー登録に失敗しました。")
		panic(err.Error())
	}
	fmt.Println("db登録 end!")

	fmt.Println("end!")
	response := "正常に処理が完了しました。"
	return response, nil
}

/**************************
   メイン
**************************/
func main() {
	lambda.Start(run)
}

3. API Gatewayの作成

REST APIでPOSTメソッドを作成し、Lambda(非VPC)を紐付けます。
特に特別な設定は不要なので省略します。

4. フロントの実装

登録画面を作成します。今回は、ユーザー名とメールアドレスが必須項目なので、その2つを登録できる入力欄と登録ボタンを簡単に作成しています。登録が完了すると「ユーザーを登録しました。」というアラートが出ます。

axiosのインストール

API Gatewayを叩くのにaxiosを使うために、プロジェクトにaxiosを追加します。

$ yarn add axios

ソースコード

axiosの使い方はaxiosライブラリを使ってリクエストするを参考にしました。

RegistrationForm.js

import React from "react";
import axios from "axios";


function RegistrationForm() {
    const API_ADD_URL = "<API Gatewayで取得したURL>"
    const [userName, setUserName] = React.useState("");
    const [email, setEmail] = React.useState("");

    const handleNameChange = event => {
        setUserName(event.target.value);
    };

    const handleEmailChange = event => {
        setEmail(event.target.value);
    }

    const handleSubmit = event => {
        axios.post(API_ADD_URL, {userName: userName, email: email})
            .then((response) => {
                if(response.data === "正常に処理が完了しました。"){
                    alert("ユーザーを登録しました。")
                    console.log(response);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
            alert("登録に失敗しました。もう一度登録してください。");
            console.log(response);
        });
        event.preventDefault();
    }

    return (
        <div>
            <h2>ユーザー登録</h2>
            <form onSubmit={handleSubmit} >
                <label >
                    ユーザー名:
                    <input type="text" value={userName} onChange={handleNameChange} /><br/>
                </label>
                <label >
                    Eメール:
                    <input type="text" value={email} onChange={handleEmailChange} /><br/>
                </label>
                <input type="submit" value="Submit" />
            </form>
        </div>
    );
}

export default RegistrationForm;

ユーザーを削除する

ユーザーから削除する場合も、基本的に登録するのと同じです。ユーザープールから削除するにはSDKのAdminDeleteUserを使用します。

実行結果

無事冒頭の完成画面のように動くようになりました!

fh4o1-lwya2.gif

おわりに

LambdaからLambdaを実行することで、NATゲートウェイを使わずにCognitoとDBの両方にユーザーを登録することができました!今考えると、Cognitoユーザープールに登録するのはAmplifyでAdmin Queries APIを使うようにして、DBに保存するのは既存のようにLambdaを呼び出すようにするのでも良かったかなとも思います!
次回は、サインインページにある、使用しないアカウント作成ボタンを消したいと思います!

17
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?