【AWS】Nuxt.js + AWS Cognito で認証付きサイトをS3にデプロイ

:::note info
この記事は、AWS Advent Calendar 2021 カレンダー2の16日目の記事です。
:::

https://qiita.com/advent-calendar/2021/aws

# はじめに
<!-- 発端や概要を記載 -->
サーバーレスアプリケーションを構築するにあたり、Amplifyを使うことで「AWSインフラの構築」「CRUDのAPIの自動生成」「フロントエンドへの SDK 提供」など、開発を効率化させるので非常に便利ですが、各々で利用しているAWSサービスが見えにくくなるのが欠点かと思います。

本記事では、Amplifyを使わずにNuxt.jsとAWS Cognitoを使用して認証機能を備えたウェブサイトを構築し、そのサイトをAWS S3にデプロイする手順について説明します。また、CircleCIを使用してCI/CDのパイプラインを設定し、コードがmainブランチにpushされると、自動的にNuxt.jsアプリがビルドされてS3にデプロイされるようにします。

# 目次
<!-- タイトルとアンカー名を編集 -->
 - [前提事項](#前提事項)
 - [全体構成図](#全体構成図)
 - [作成する機能](#作成する機能) 
 - [Cognitoの準備](#cognitoの準備)
 - [S3の準備](#s3の準備)
 - [Nuxt.js プロジェクトのセットアップ](#Nuxt.js プロジェクトのセットアップ)
 - [Nuxt.jsとCognitoの統合](#Nuxt.jsとCognitoの統合)
 - [ログイン画面の作成](#ログイン画面の作成)
 - [CI/CDの準備](#cicdの準備)
 - [S3上での確認](#s3上での確認)
 - [まとめ](#まとめ) 
 - [参考文献](#参考文献)

<!-- 前提事項 -->
<a id="#Chapter1"></a>
# 前提事項
 - Nuxt.jsを使ってみたい人
 - AWS Cognitoを使ってみたい人
 - ログイン処理を作るのが面倒な人
 - aws-cliは事前にインストール済であること
    - [参考](https://qiita.com/YuukiMiyoshi/items/edc2640ed4e842631f2d)
 - CI/CDはCircleCIを利用します

<!-- 全体構成図 -->
<a id="#Chapter2"></a>
# 全体構成図
![MailDemo.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295139/af4dd6a8-0e7e-b0e9-3c9b-ff9bd04fd239.png)

作成したNuxt.jsはS3上でホスティングしております。

<!-- 作成するもの -->
<a id="#Chapter3"></a>
# 作成する機能

 - ログイン画面
![screencapture-s3nuxt-kemper0530-signin-2021-12-13-09_46_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295139/1afdc4d4-cb8c-054f-2937-ae9cb339ab94.png)

 - トップページ
![screencapture-s3nuxt-kemper0530-2021-12-14-08_31_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295139/10fd8b21-c2cb-87fb-eb0c-7b2e0ec6e3d1.png)


ユーザ登録画面でアカウントを作成し、ログイン画面でログインに成功したらトップページへ
遷移させます。

実際のソースはこちらになります。

https://github.com/KEMPER0530/cognito-demo

<!-- Cognitoの準備 -->
<a id="#Chapter4"></a>
# Cognitoの準備

AWS Cognito は、ウェブおよびモバイルアプリの認証、承認、およびユーザー管理機能を提供します。
ユーザーは、ユーザー名とパスワードを使用して直接ログインするか、Facebook、Amazon、Google、Apple などのサードパーティーを通じてログインすることができます。

Coginitoのユーザプールは、AWSのコンソール画面からユーザを作成できるものの、仮パスワードを設定してユーザにパスワードを強制変更してもらわないといけないので、今回はaws-cliを利用して作成します。

#### 1.ユーザプールの作成
「pool-name」部分は任意でお願いします

```
$ aws cognito-idp create-user-pool --pool-name test_pool
```


#### 2.ユーザプール確認

```
$ aws cognito-idp list-user-pools --max-results 10 | jq ".UserPools[] | {Id, Name}"
```
以下は出力例です。出力される実行結果からIDを控えておきます。

```
{
  "Id": "ap-northeast-1_XXXXXXX",
  "Name": "test_pool"
}
```

#### 3.ユーザ作成
2.控えたIDを利用してユーザを作成します。こちらは後続の「[認証ミドルウェアの準備](#認証ミドルウェアの準備)」でも利用いたします。

```
$ aws cognito-idp admin-create-user \
--user-pool-id "ap-northeast-1_XXXXXXX" \
--username "test_mail@example.com" \
--user-attributes Name=email,Value="test_mail@example.com" Name=email_verified,Value=true \
--message-action SUPPRESS
```

user-pool-id : 2.で控えたuser-pool-idを入力
username : ログイン用のメールアドレスを入力
user-attributes : 認証時のオプション(ここではメールアドレスのみ認証とさせてます)
message-action : SUPPRESS (対象ユーザへの通知を止める)

ユーザ作成結果

```json
{
    "User": {
        "Username": "test_mail@example.com", 
        "Enabled": true, 
        "UserStatus": "FORCE_CHANGE_PASSWORD", 
        "UserCreateDate": 1639385020.166, 
        "UserLastModifiedDate": 1639385020.166, 
        "Attributes": [
            {
                "Name": "sub", 
                "Value": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
            }, 
            {
                "Name": "email_verified", 
                "Value": "true"
            }, 
            {
                "Name": "email", 
                "Value": "test_mail@example.com"
            }
        ]
    }
}
```

#### 4.パスワードの恒久化

現状```UserStatus```が ```FORCE_CHANGE_PASSWORD``` となっているため、このままではログインできません。```admin-set-user-password```を使ってパスワードを設定します。ユーザプールID、ユーザネーム、パスワードをそれぞれ指定します。

```
$ aws cognito-idp admin-set-user-password \
--user-pool-id "ap-northeast-1_XXXXXXX" \
--username "test_mail@example.com" \
--password 'P@ssw0rd' \
--permanent
```

```--permanent``` オプションで、パスワードを恒久化します。```--no-permanent```にすると仮パスワードになります。

#### 5.ユーザ確認

```
$ aws cognito-idp admin-get-user \
--user-pool-id "ap-northeast-1_XXXXXXX" \
--username "test_mail@example.com"
```

確認結果

```json
{
    "Username": "test_mail@example.com", 
    "Enabled": true, 
    "UserStatus": "CONFIRMED", 
    "UserCreateDate": 1639385434.063, 
    "UserAttributes": [
        {
            "Name": "sub", 
            "Value": "XXXXXXXXXXXX-XXXX-XXX-XXX-XXXXXXXXXXXX"
        }, 
        {
            "Name": "email_verified", 
            "Value": "true"
        }, 
        {
            "Name": "email", 
            "Value": "test_mail@example.com"
        }
    ], 
    "UserLastModifiedDate": 1639385519.298
}
```

```UserStatus```が ```CONFIRMED``` になっています。これで ユーザ名```test_mail@example.com```、パスワード```P@ssw0rd```でログインできるようになります。

#### 6.アプリクライアントの追加

```
$ aws cognito-idp create-user-pool-client --user-pool-id "ap-northeast-1_XXXXXXX" --client-name "test-pool" \
--explicit-auth-flows "ALLOW_ADMIN_USER_PASSWORD_AUTH" "ALLOW_USER_SRP_AUTH" "ALLOW_REFRESH_TOKEN_AUTH" \
--prevent-user-existence-errors "ENABLED"
```
出力結果
ClientIdを控えて置きます。後続の「[認証ミドルウェアの準備](#認証ミドルウェアの準備)」で利用します。

```json
{
    "UserPoolClient": {
        "UserPoolId": "ap-northeast-1_XXXXXXX", 
        "LastModifiedDate": 1639401558.11, 
        "ClientId": "XXXXXXXXXXXXXXXXXXXXXXXXXX", 
        "AllowedOAuthFlowsUserPoolClient": false, 
        "PreventUserExistenceErrors": "ENABLED", 
        "ExplicitAuthFlows": [
            "ALLOW_ADMIN_USER_PASSWORD_AUTH", 
            "ALLOW_USER_SRP_AUTH", 
            "ALLOW_REFRESH_TOKEN_AUTH"
        ], 
        "RefreshTokenValidity": 30, 
        "CreationDate": 1639401558.11, 
        "ClientName": "test-pool"
    }
}
```

<!-- S3の準備 -->
<a id="#Chapter5"></a>
# S3の準備

こちらもaws-cliを利用して構築していきます。

#### 1.公開用のS3バケットを作成

```
$ aws s3 mb s3://[backet-name]
```

#### 2.バケットのアクセスブロックを設定(公開できる状態へ)

```
$ aws s3api put-public-access-block --bucket [backet-name] --public-access-block-configuration  "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false"
```

#### 3.バケットポリシーの作成
下記のjsonファイルをローカルに作成します。
[backet-name]のところは作成した名称へ置き換えてください。

setting-policy.json

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow", 
            "Principal": "*", 
            "Action": "s3:GetObject", 
            "Resource": "arn:aws:s3:::[backet-name]/*" 
        } 
    ] 
}
```

S3バケットに上記ポリシーファイルをアタッチします。

```
$ aws s3api put-bucket-policy --bucket [backet-name] --policy file://setting-policy.json
```

#### 4.静的ウェブサイトホスティングの設定

```
$ aws s3 website s3://[backet-name] --index-document index.html
```

これでS3の準備は完了です。

---

<!-- Nuxt.js プロジェクトのセットアップ -->
<a id="#Chapter6"></a>
# Nuxt.js プロジェクトのセットアップ

まず、Nuxt.js プロジェクトをセットアップします。Nuxt.jsはVue.jsのフレームワークであり、サーバーサイドレンダリング(SSR)、Vue Router、Vuexなどの設定を自動的に行います。以下のコマンドでNuxt.jsプロジェクトを生成します。

```
$ npx create-nuxt-app test-form

create-nuxt-app v3.6.0
✨  Generating Nuxt.js project in nuxt-inquiry-form
? Project name: nuxt-inquiry-form
? Programming language: TypeScript
? Package manager: Npm
? UI framework: Tailwind CSS
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: GitHub Actions (GitHub only)
? What is your GitHub username? GitHubに登録されているユーザー名
? Version control system: Git
```

<!-- Nuxt.jsとCognitoの統合 -->
<a id="#Chapter7"></a>
# Nuxt.jsとCognitoの統合
Nuxt.jsとCognitoを統合するために、@nuxtjs/authモジュールを利用します。このモジュールは、簡単な設定で多くの認証プロバイダと統合することができます。@nuxtjs/authモジュールをインストールし、nuxt.config.jsに設定を追加します。ここではCognitoを認証プロバイダとして設定し、ユーザープールのIDとクライアントIDを設定します。

#### 1.nuxt/authの利用
authモジュールはNuxt.jsで利用できる認証モジュールです。authを使うことによって非常に簡単にログイン・ログアウト等の機能を実装することができます。

#### 2.authおよびaxiosモジュールを追加

```
$ yarn add --exact @nuxtjs/auth-next
$ yarn add @nuxtjs/axios
```

#### 3.@a1ter/nuxt-auth-aws-cognito-scheme のインストール
nuxt/auth はデフォルトでは Cognito に対応していないため、 @a1ter/nuxt-auth-aws-cognito-scheme をインストールします。

```
$ yarn add @a1ter/nuxt-auth-aws-cognito-scheme
```

#### 4.nuxt.config.jsの設定
ビルドしてS3で環境変数を利用できるようにする設定します

```
$ yarn add @nuxtjs/dotenv
```

```js
require("dotenv").config()
const {
    AWS_APPSYNC_REIGION,
    AWS_APPSYNC_GRAPHQL_ENDPOINT,
    AWS_APPSYNC_AUTHENTICATION_TYPE,
    AWS_APPSYNC_APIKEY,
    AWS_COGNITO_REIGION,
    AWS_COGNITO_USERPOOLID,
    AWS_COGNITO_USERPOOLWEBCLIENTID
} = process.env

export default {
    env: {
        AWS_APPSYNC_REIGION,
        AWS_APPSYNC_GRAPHQL_ENDPOINT,
        AWS_APPSYNC_AUTHENTICATION_TYPE,
        AWS_APPSYNC_APIKEY,
        AWS_COGNITO_REIGION,
        AWS_COGNITO_USERPOOLID,
        AWS_COGNITO_USERPOOLWEBCLIENTID
    },
```

amplify.jsの設定、Amplifyは使いませんがライブラリは利用します

```js
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false,

// Target: https://go.nuxtjs.dev/config-target
target: 'static',

// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [{ src: '@/plugins/amplify.js', mode: 'client' }, ],
```

認証ミドルウェアを設定します

```js
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
    '@nuxtjs/auth-next'
],
router: {
    middleware: ['auth'],
},
```

authモジュール内でCognitoを利用できるように設定します

```js
auth: {
    redirect: {
        login: '/signin', // 未ログイン時に認証ルートへアクセスした際のリダイレクトURL
        logout: '/signin', // ログアウト時のリダイレクトURL
        home: false, // ログイン後のリダイレクトURL
    },
    localStorage: false,
    strategies: {
        cognito: {
            scheme: '@a1ter/nuxt-auth-aws-cognito-scheme/scheme/scheme',
            credentials: {
                userPoolId: process.env.AWS_COGNITO_USERPOOLID,
                userPoolWebClientId: process.env.AWS_COGNITO_USERPOOLWEBCLIENTID,
                region: process.env.AWS_COGNITO_REIGION
            },
            endpoints: {
                user: false,
            },
        },
    },
},
```

次に```plugins/amplify.js```を作成します


```js
import Vue from 'vue'
import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin, components } from 'aws-amplify-vue'
import awsconfig from '@/src/appsync-exports'
import Auth from '@aws-amplify/auth';

export default () => {
    Auth.configure({
        Auth: {
            region: process.env.AWS_COGNITO_REIGION,
            userPoolId: process.env.AWS_COGNITO_USERPOOLID,
            userPoolWebClientId: process.env.AWS_COGNITO_USERPOOLWEBCLIENTID,
        },
    })
}

Amplify.configure(awsconfig)

Vue.use(AmplifyPlugin, AmplifyModules)
Vue.component(components)
```

環境変数の設定値
- process.env.AWS_COGNITO_REIGION : ap-north-east-1
- process.env.AWS_COGNITO_USERPOOLID : COGNITOのユーザプールID
- process.env.AWS_COGNITO_USERPOOLWEBCLIENTID : COGNITOのユーザクライアントID

<!-- ログイン画面の作成 -->
<a id="#Chapter8"></a>
# ログイン画面の作成

#### 1.トップページの作成
まず```/pages/index.vue```を作成します。
ログイン後に遷移する画面となります。ログイン後に認証をクリアするログアウトボタンを設定します。

```vuejs
<template>
  <div class="m-auto flex justify-center min-h-screen items-center text-center bg-black">
      <div>
        <div class="flex flex-col">
          <button class="py-4 px-8 mt-8 text-2xl text-gray-50 bg-red-500 hover:bg-red-600 focus:ring-red-600 focus:ring-offset-red-600 text-white transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-full" @click.prevent="$auth.logout()">Logout</button>
        </div>
      </div>
  </div>
</template>
```

#### 2.ログイン画面の作成
次に```/pages/Signin.vue```を作成します。

```vuejs
<template>
  <div  class="m-auto flex justify-center min-h-screen items-center text-center bg-black">
    <Signin />
  </div>
</template>
```

詳細の```/component/Signin.vue```を作成します。
cssはgithubを参照ください。

```vuejs
<template>
  <div class="form-wrapper">
    <h1><b>Sign In</b></h1>
    <div>
      <div class="form-item">
        <label for="email"></label>
        <input type="email" name="email" required="required" placeholder="Email Address" v-model="email">
      </div>
      <div class="form-item">
        <label for="password"></label>
        <input type="password" name="password" required="required" placeholder="Password" v-model="password">
      </div>
      <div class="button-panel">
        <button class="signin-button" title="Sign In" @click="signin">Sign In</button>
      </div>
    </div>
    <div class="form-footer">
      <nuxt-link to="/signup">Create an account</nuxt-link>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data: () => ({
    email: '' as string,
    password: '' as string,
  }),
  head() {
    return {
    }
  },
  methods: {
    async signin() {
      // try {
        const username = this.email;
        const password  = this.password;
        await this.$auth.loginWith('cognito', {
          data: {
            username,
            password,
          }
        }).then((success) => {
           console.log("success sign in.... ");
           // @ts-ignore
           this.$router.push({ path: '/' });
        }).catch((error) => {
           alert(error.message);
           console.log('error signing in:', error.code + ":" + error.message + ":" + error.name);
           return;
        });
    },
  },
})
</script>
```

#### 3.ローカルでの確認
ここまでできたらローカルでも確認することができます。
下記コマンドを実行し画面を立ち上げて、「[Cognitoの準備](#Cognitoの準備)」で設定したID、パスワードでログイン処理を実施してください。

```
$ yarn dev
```


<!-- CI/CDの準備 -->
# CI/CDの準備
CircleCIを使ってCI/CDパイプラインを設定します。以下の```.circleci/config.yml```ファイルの設定を使用します。この設定により、コードがmainブランチにpushされるたびにCircleCIがNuxt.jsをビルドし、S3にデプロイします。

```yml
version: 2.1
orbs:
  aws-s3: circleci/aws-s3@3.0
defaults: &defaults
  working_directory: ~/aws-rough
  docker:
    - image: cimg/python:3.9.6-browsers
filter_only_main: &filter_only_main
  filters:
    branches:
      only: main
jobs:
  install:
    <<: *defaults
    steps:
      - checkout
      - restore_cache:
          name: Restore Yarn Package Cache
          keys:
            - yarn-packages-{{ checksum "yarn.lock" }}
      - run: yarn install
      - save_cache:
          name: Save Yarn Package Cache
          key: yarn-packages-{{ checksum "yarn.lock" }}
          paths:
            - ~/.cache/yarn
      - persist_to_workspace:
          root: ~/aws-rough
          paths:
            - ./*
  test:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/aws-rough
      - run: yarn test

  build:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/aws-rough
      - run: yarn generate
      - persist_to_workspace:
          root: ~/aws-rough
          paths:
            - dist

  deploy:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/aws-rough
      - aws-s3/sync:
          arguments: |
            --cache-control "max-age=86400"
          from: ./dist/
          to: 's3://${BACKET_NAME}'

workflows:
  version: 2
  test-deploy:
    jobs:
      - install
      - test:
          requires:
            - install
      - build:
          <<: *filter_only_main
          requires:
            - install
            - test
      - deploy:
          <<: *filter_only_main
          requires:
            - install
            - test
            - build

```

CircleCIに設定する環境変数
- AWS_ACCESS_KEY_ID : アクセスキー
- AWS_SECRET_ACCESS_KEY : シークレットアクセスキー
- AWS_REGION　：　S3を利用しているリージョン
- AWS_COGNITO_REIGION　：　Cognitoを利用しているリージョン
- AWS_COGNITO_USERPOOLID　：　CognitoのユーザプールID
- AWS_COGNITO_USERPOOLWEBCLIENTID　：　CognitoのユーザクライアントID
- BACKET_NAME : S3で作成したバケット名

<b>CircleCIの実行</b>
![スクリーンショット 2021-12-15 21.41.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295139/4286f4ea-3f91-9e99-ffab-93161da48e7b.png)

S3へデプロイ完了！

<!-- S3上での確認の準備 -->
# S3上での確認

S3にアップロードし、ブラウザ上で表示されるか、確認します。
URLは下記となります。
http://[bucket-name].s3-website-[region].amazonaws.com

![screencapture-s3nuxt-kemper0530-signin-2021-12-13-09_46_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295139/96bf8666-be17-ce93-f169-9f6f723dff8b.png)

表示されました！

<!-- まとめ -->
<a id="#Chapter10"></a>
# まとめ
 - 今回Amplifyを使わなくても「nuxt/auth」を利用することで簡単にCognitoと連携してログイン処理を作成することができました。
 - 詳細な認証の仕組みを本記事では説明しておりませんので参考文献の「nuxt/authの利用方法」を確認頂ければと思います。

最後まで読んでいただきありがとうございました。

<!-- 参考文献 -->
<a id="#reference"></a>
# 参考文献
- [nuxt/authの利用方法](https://auth.nuxtjs.org/)
- [AWS CognitoのCLI](https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/create-user-pool.html)