Serverless FrameworkとCognitoを使ったユーザー認証システムをさっくり構築するハンズオン



どうも[@kwn-hrk](https://qiita.com/kwn-hrk)です。
この記事は[ハンズラボ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/handslab) 15日目の記事です。


# はじめに

サービスを作る上で必要となるケースが多い**ユーザー認証システム**。
でもいざ作るとなると、ユーザー登録機能やメールアドレス検証機能、ログイン機能など…。
だいぶ、手間がかかりますよね。

そんなあなたに朗報です。
AWSのCognitoを使用したユーザー認証システムをさっくり構築できるハンズオン。
本記事で紹介します。

後述の対象者に当てはまる方がいれば、一度試してみてください。

また、あくまでもハンズオンなので、各サービスの詳細に関する説明は基本省いております。


# 対象者

- Serverless Frameworkを利用したことがある人、
- Cognitoについてふんわり理解してる人、触ってみたい人
- Cognitoを用いた、認証基盤の作成に興味のある人
- 一旦動くユーザー認証基盤が欲しい人、試してみたい人

# 前提
- Serverless Frameworkがinstallされていること。
    - 執筆時の私のバージョンは以下でした。
        - Framework Core: 2.62.0
        - Plugin: 5.4.6
        - SDK: 4.3.0
        - Components: 3.17.1
- 構築先AWSアカウントのアクセスキー、シークレットキーを利用できる状態である。
- 上記AWSアカウントにServerless Frameworkでdeployするための十分な権限が付与されていること。
    - deploy時、権限エラーになりましたら都度追加していくことになります。


# ユーザー認証システムについて

今回構築するシステムの説明です。

## 仕様

1. メールアドレス、パスワードでユーザー登録できる。
1. 登録後、本人確認のため、認証コードを送信、認証コード、メールアドレスを用いて本人確認を実施する。
1. ログイン後、払い出されたTokenを用いることで一部APIを利用することができる。

上記3点を実現します。

## ユーザー認証フロー

登録したいユーザーが踏む認証フローになります。
こちら、本記事の挙動確認の際、フローを体験することができます。

1. メールアドレス、パスワードを用いてユーザーに仮登録していただきます。
2. 仮登録に用いたメールアドレスに認証コードが届きます。
3. 仮登録に用いたメールアドレス、認証コードを用いて、本登録を行います。
4. 仮登録に用いたメールアドレス、パスワードを利用してログインします。
5. 払い出されたトークンを用いて、ユーザー認証が必要なAPIを利用します。

## AWS利用技術

利用しているAWSサービスです。

- AWS
    - Api Gateway
    - Cognito
    - Lambda

### 少しだけCognitoについて

今回認証基盤には、Cognitoユーザープールを使用しております。

Cognitoとは認証、承認、ユーザの管理機能を提供するAWSのサービスです。

ユーザープールというのは、認証を行うにあたり、対象ユーザーが登録されているのかどうかを検証することができ、登録済みユーザーであれば、引き続きサービスを利用できるよう連携してくれるものです。


https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html


## ユーザー認証システムアーキテクチャ図

構築されるアーキテクチャです。
ユーザーはApi Gatewayを通して、各APIにアクセスできますが、helloAPIだけは、Cognitoユーザープール認証済みユーザのみアクセスできる構成です。

![qiita_アーキテクチャ-Page-1.drawio (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/c95912bc-f758-5056-784c-781cade4c531.png)


# さっくり構築

手順、多めに見えますが、やってみるとあっという間なので、ぜひお試しください！

※キーやIDの取り扱いにはご注意ください。決してGitなどにはあげないように！！
※Cognito情報周り本当は環境変数として定義したかったですが、ご愛嬌ということで！！

1. [こちら](https://github.com/hrk-kwn/simple_authorization_project)をclone
1. deploy対象AWSアカウントのキー情報をlocalのprofileファイルに記載
2. 定義したprofile名を用いて、コマンド実行 
   1. sls deploy --verbose --aws-profile profile名
3. deploy成功時、outputされる以下情報を控える。
   1. CognitoUserPoolIdARN 
   2. CognitoUserPoolClientId 
   3. CognitoUserPoolId
4. 各Lambdaの下記該当箇所にコピペしてください
   1. entry_tmp.py
      1. line:10 aws_access_key_id=皆様の環境で利用できるアクセスキー
      2. line:11 aws_secret_access_key=皆様の環境で利用できるシークレットキー
      3. line:15 ClientId = CognitoUserPoolClientId
   2. entry_prd.py
      1. line:10 aws_access_key_id=皆様の環境で利用できるアクセスキー
      2. line:11 aws_secret_access_key=皆様の環境で利用できるシークレットキー
      3. line:15 ClientId = CognitoUserPoolClientId
   3. login.py
      1. line:10 aws_access_key_id=皆様の環境で利用できるアクセスキー
      2. line:11 aws_secret_access_key=皆様の環境で利用できるシークレットキー
      3. line:15 UserPoolId = CognitoUserPoolId
      4. line:16 ClientId = CognitoUserPoolClientId
5. 再度deployしてください
   1. sls deploy --verbose --aws-profile <profile名>


構築ができましたら各サービスをAWSコンソールで確認してみましょう。

**Cognito User Pool**

Cognito一覧からリソースを確認します。

![Cognito一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/b22e4318-80ad-ff85-8828-b190a07af218.png)


無事作成されてます。
一応詳細も見てみましょう。

![Cognito詳細.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/5689f1ab-00a6-f861-5c10-0fda6fc735d2.png)


問題なさそうです。ユーザー数もまだ０ですね。
後々登録を行なった際、こちらのカウント数が増えるので、確認しましょう。

**API Gataway**

次にAPI Gatawayも確認してみましょう。

![FireShot Capture 042 - API Gateway - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/d25e5ed1-d23b-fcff-536f-ea3118c37b95.png)


各APIのエンドポイントも問題なく構築されていますね。
唯一Cognitoオーサライザー設定を入れたhelloAPIの詳細も確認してみましょう。

![FireShot Capture 043 - API Gateway - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/c2ea1a98-ae1d-d11f-5643-3531afbd27b7.png)


認可のところに、COGNITO_USER_POOLSと書かれていれば問題ありません。
これでhelloAPIはCognito認証がされていないと使えない状態になっています。


**Lambda**

最後にLambdaを確認します。

![lambda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/50e58fc8-7d1d-cc34-397d-d04efdd6f32b.png)


それぞれ構築されてるので問題なさそうです。

上記リソース全てが構築されていれば、いよいよ挙動確認に入ります。

# さっくり挙動確認

deploy成功結果に出力されているendpointsを利用します。

```
endpoints:
  GET - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello
  POST - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/entry/tmp
  POST - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/entry/prd
  POST - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/login
```

## 1. 仮登録を行う

登録したいメールアドレス、パスワードを用いて、仮登録APIを叩いてください。

```
curl -X POST -H "Content-Type: application/json" \
  -d '{"email":"xxxxxxxx@xxx.xxx", "password":"xxxxxxxxx"}' \
  https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/entry/tmp
...
{"message": "tmp successful"}
```

messageが **tmp successful**かつ、対象のCognitoに先ほど登録したメールアドレスに紐づいたユーザーがいれば正常です。

![FireShot Capture 045 - simple-authorization-project-dev-user-pool - ユーザープール_ - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/a2c71d02-f928-e581-6f31-886b5286d4e8.png)

大丈夫そうですね。

Eメール確認済み項目で、このユーザーが使用しているメールアドレスが本当に存在するものなのかを証明します。現在仮登録まで済ませているだけなので、「いいえ」となっております。

また今回使用しませんが、Cognitoユーザープールから登録したユーザーに対して払い出す一意なユニークIDがユーザー名項目に登録されています。Cognito内のユーザーを特定する場合や、他DBと連携する際、このユニークIDを外部キーとして活用することもできます。

※設定によってはユーザー名をニックネーム形式にすることも可能です。


それでは、仮登録に使用したメールアドレス宛に認証コードが届いていると思うので、コピペしておきましょう。

![認証コード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/f2af4d27-a9ba-fb7d-8467-9ca522a9b8aa.png)


## 2. 本登録を行う

仮登録時のメールアドレス、コピペした認証コードを用いて、本登録APIを叩いてください。  

```
curl -X POST -H "Content-Type: application/json" \
  -d '{"email":"xxxxxxxx@xxx.xxx", "confirmation_code":"xxxxxx"}' \
  https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/entry/prd
...
{"message": "prd successful"}
```

messageが **prd successful**かつ、対象のCognitoにある先ほど登録したメールアドレスに紐づいたユーザーのEメール確認済み項目が変更されていれば正常です。

![FireShot Capture 047 - simple-authorization-project-dev-user-pool - ユーザープール_ - ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156169/dfca45ba-c0a5-979a-bed7-b6e9236a08c4.png)

Eメール確認済み項目が「はい」に変わっているので、大丈夫そうです。
これでCognitoにユーザーとして認められた状態になりました。


## 3. ログインを行う

仮登録時のメールアドレス、パスワードを用いて、ログインAPIを叩いてください。

```
curl -X POST -H "Content-Type: application/json" \
  -d '{"email":"xxxxxxxx@xxx.xxx", "password":"xxxxxxxxx"}' \
  https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/login
...
{"message": "login successful", "AccessToken": "eJraW...", "RefreshToken": "eyJjd....", "IdToken": "eyJra..."}
```

messageが **login successful**かつ、Cognitoユーザープールから払い出されたTokenがレスポンスに含まれていれば正常です。

次の手順で**IdToken**使うので、コピペしておきましょう。


## 4. helloAPIを叩く

手順３で返却された**Idtoken**をヘッダーAuthorization属性に設定して、helloAPIを叩いてください。
 
messageが **Hello. CognitoUser!"** なら正常です。

```
curl https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H 'Authorization:eyJra...'
...
{"message": "Hello. CognitoUser!"}
```

ユーザーはApi Gatewayを通して、各APIにアクセスすることはできますが、helloAPIだけは、Cognitoユーザープール認証済みユーザのみアクセスできる構成が実現しました:muscle:


### 4.5.　（おまけ）異常系のアクセスについて

異常系のリクエストが来た場合、Lambdaまで処理いかず、Api Gateway側で事前に検証を行ってくれます。そして何かエラーがある場合、その旨をユーザーに返却してくれます。

- IdTokenの内容を改変して通信を行なった場合、ApiGataway側で403:AccessDeniedエラーとして処理されます

```
curl https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H 'Authorization:eyJda...'
...
{"Message":"Access Denied"}
```

- IdTokenを設定せず通信を行なった場合、ApiGataway側で401:Unauthorizedエラーとして処理されます


```
curl https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H 'Authorization:'
...
{"message":"Unauthorized"}
```

各エラー時のレスポンスもApiGateway側の設定でカスタマイズできます。


これにて挙動確認終了です！！:clap:
serverless.ymlの記述は今構築できているものと見比べながら調整していくといいと思います。
ここに関しては、習うより慣れろ派です:flushed:（本当は細かく書きたかった！！！！）


## 後片付け

一通り挙動確認が済みましたら、削除しちゃいましょう。
以下、コマンドでお掃除完了です。

```
sls remove --verbose --aws-profile profile名
```





# さいごに

今回構築したユーザー認証システムは**登録をすれば、helloAPIを叩けるようになる**だけでしたが、
みなさまのサービス導入パターンによっては、Cognito認証を通して、ログイン管理をしたり、Cognitoから払い出される一意なID使って、他resourceと連携したりと、ユースケースはたくさんあると思います。

また本記事では触れていませんが、CognitoにはIDプールなるものも存在するので、興味のある方はぜひそちらも触ってみてはいかがでしょうか！

この記事が少しでも誰かの役に立つことを祈っております。
最後までお付き合いいただきありがとうございました！

それではみなさま、よき、認証ライフを！！