[備忘録] Cognito と Vue.js でログイン機能を実装

CognitoとVue.jsでログイン機能を実装してみた。　　
ただしCognito の構築は、[[備忘録] AWS CDK プロジェクトの作成](https://qiita.com/m-chan/items/011a93e40f26230df135)で作成した cdk プロジェクトを利用した。

# はじめに
### cognitoとは
> Amazon Cognito はウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。これは、ユーザーディレクトリ、認証サーバー、OAuth 2.0 アクセストークンと AWS 認証情報の承認サービスです。

cognito を用いると、ユーザーのサインアップ、サインイン、アクセスコントロール等の機能を早く簡単に実現することができる。

# 実施したこと

1. cognito の作成
1. vue.js でログイン画面の作成とルーティングの設定
1. 動作確認

### 1. cognito の作成

```app-stack.ts
// cognito
const userPool = new UserPool(this, 'app-user-pool', {
  userPoolName: 'app-user-pool',
  signInAliases: { email: true }, 
  removalPolicy: RemovalPolicy.DESTROY,
});

// application client
userPool.addClient('AppApplication', {
  userPoolClientName: 'app-application', 
  generateSecret: false,
});
```
アカウント作成はコンソールから行い、存在するメールアドレスとパスワードをログインページから送信することでログインさせることを想定しているので、signInAliases は email を指定し、サインアップに必要な設定は省いた。  

またアプリケーションクライアントの設定において、[JavaScript からは、クライアントシークレットが有効になっていていては利用できません](https://gb-j.com/column/aws-cognito-javascript/)という記事を見つけたので、generateSecret は false としている。

デプロイ後、コンソール該当のユーザープールが確認できたので、ユーザーの作成を行なった。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280374/91b4553e-e8ee-b15b-bb86-f3294e6874b9.png)

ユーザー作成直後は下記のようにステータスがパスワードを強制的に変更と出て、パスワードを変更しないと使用できないが、vueでサインアップやパスワード再設定のページを作成する予定はないので、CLI で パスワードの再設定を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280374/8f9824d2-2aed-c877-73ec-f61d60f1e5f4.png)


ターミナル上で下記コマンドを実行した。
```
% userName=****
% password=Password_002
% userPoolId=ap-northeast-1_*****
% clientId=*****
% aws cognito-idp admin-set-user-password --user-pool-id ${userPoolId} --username ${userName} --password ${password} --permanent
```

すると、ステータスが確認済みに変わったことを確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280374/7126eb0b-aadd-db09-0e64-e0239a2c6c31.png)

これで cognito の設定は完了。
次にログイン画面を作成していく。

### 2. vue.js でログイン画面の作成

vueのバージョンは、vue3を選択し、vue.js プロジェクトを作成した。
```
% vue create app
```

下記構成でフォルダとファイルが作成された。
```
├── README.md		
├── public
     └── favicon.ico			
     └── index.html			
├── src
     └── assets
          └── logo.png
     └── components
          └── HellowWorld.vue
     └── App.vue
     └── main.js
├── gitignore
├── node_modules		
├── babel.config.js
├── jsconfig.json		
├── package-lock.json
├── vue.config.js		
├── package.json
```

必要なパッケージのインストールは下記コマンドで行える。
```
% cd app
% npm i amazon-cognito-identity-js
% npm install vue-router@4
% npm install bootstrap@4.6.0
```

また、bootstrapを使用するため、main.jsに下記2行追加している。
```main.js
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.min.js'
```

ログイン画面の login.vue および、ログイン後遷移する top.vue のファイルを新たに作成した。
```
% cd src
% mkdir pages
% cd pages
% touch login.vue
% touch top.vue
```
.envにはCognitoのプールIDとクライアントIDを記しておく。ここで1点注意すべきは、変数名は "VUE_APP_" から始まる必要があるということだ。
```.env
VUE_APP_POOL_ID="*****"
VUE_APP_CLIENT_ID="*****"
```

```router.js
import { createRouter, createWebHistory } from 'vue-router';
import Login from '../src/pages/Login.vue';
import Top from '../src/pages/Top.vue';
import {
  CognitoUserPool,
} from 'amazon-cognito-identity-js'

const routes = [
  {
    path: '/',
    name: 'login',
    component: login,
  },
  {
    path: '/top',
    name: 'top',
    component: top,
    meta: { requiresAuth: true }
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

router.beforeEach(async (to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    var poolData = {
      UserPoolId: process.env.VUE_APP_POOL_ID,
      ClientId: process.env.VUE_APP_CLIENT_ID,
    };
    var userPool = new CognitoUserPool(poolData);
    const cognitoUser = userPool.getCurrentUser();
    console.log(cognitoUser);
    if (cognitoUser != null) {
      cognitoUser.getSession(function(err, session) {
        if (err) {
          next('/');
        } else {
          cognitoUser.getUserAttributes(function(err, result) {
            if (result.lentgh !== 0) {
              next();
            } else {
              next('/');
            }
          });
        }
      });
    } else {
      next('/');
    }
  }
  next();
});

export default router;
```

```login.vue
<template>
  <div class="container rounded-circle login">    
    <form>
      <div class="mb-3">
        <input type="email" class="form-control" id="inputEmail" aria-describedby="emailHelp" v-model="inputEmail" placeholder="email address">
      </div>
      <div class="mb-3">
        <input type="password" class="form-control" id="inputPassword" v-model="inputPassword" autocomplete="on" placeholder="password">
      </div>
      <button @click.prevent="login" class="btn mx-auto d-block">Login</button>
    </form>
  </div>
</template>


<script>
import { ref } from 'vue'
import {
  CognitoUserPool,
  CognitoUser,
  AuthenticationDetails

} from 'amazon-cognito-identity-js'

export default {
  setup() {
    const inputEmail = ref('')
    const inputPassword = ref('')

    const login = async () => {
      var poolData = {
        UserPoolId: process.env.VUE_APP_POOL_ID,
        ClientId: process.env.VUE_APP_CLIENT_ID,
      };
      var userPool = new CognitoUserPool(poolData);

      //cognitoパラメータ設定
      var username = inputEmail.value;
      var password = inputPassword.value;

      var authenticationData = {
        Username: username,
        Password: password,
      };
      var authenticationDetails = new AuthenticationDetails(
        authenticationData
      );

      var userData = {
        Username: username,
        Pool: userPool,
      };

      var cognitoUser = new CognitoUser(userData);
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: function(result) {
          var top="/top";
          location.assign(top);
        },
        onFailure: function(err) {
          alert(err.message || JSON.stringify(err));
        }
      });
    }
    return {
      inputEmail,
      inputPassword,
      login
    }
  }
}
</script>
<style>
.container.login {
  margin: 10% auto;
  padding: 20%;
  background: linear-gradient(#9fa09e, #898989);
}
form {
  margin: 10%;
}
.btn.mx-auto.d-block{
  display: inline-block;
  background-color: #e73562;
  color: #ffffff;
}
</style>
```

### 3. 動作確認
下記コマンドを実行し、localhost:8080 にアクセスすると、ログインページが表示された。
```
% npm run serve
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280374/2744a132-8e7d-dbbf-4ded-e6d7d6023148.png)

まずはじめに、誤ったメアドまたはパスワードを入力してみた。すると、 User does not exit. のアラートが表示され、ログインできない。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280374/60e040c4-bcd1-9b41-8d8a-e2c180f58b7f.png)

次に、そのまま localhost:8080/top をブラウザに直接入力し、ログイン後のページにアクセスできるか試してみた。まだログインしていないのでここではログインページにリダイレクトして欲しいところであり、実際に期待通りの挙動を示した。

次に、正しいメアドとパスワードを入力してログインボタンを押し、/top に遷移することを確認した。

最後に、ログイン状態でログインページ(localhost:8080)に戻ってみた。UXを考えると、自動ログインが走り top ページにリダイレクトしても良いが、今回はその実装はしていないため、そのままログインページが表示される。ただし、ログインボタンを押さなくてもログイン状態であれば、直接URLを叩いて /top に遷移することは可能である。

# 最後に
Cognito と Vue3 でログイン機能を実装した。  
ただし今回は、新規登録やパスワード変更といった機能は考慮せず、必須項目を正しく入力したらログイン、間違ったらアラートという簡単な実装にとどめた。

参考にした文献を記す。
- https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html
- https://qiita.com/yamato1491038/items/bbef1df21e54e22d36ca
- https://cloud5.jp/aws-cli-cognito-confirmed/
- https://gb-j.com/column/aws-cognito-javascript/

- https://bftnagoya.hateblo.jp/entry/2021/10/07/165307
- https://stackoverflow.com/questions/71271760/parsing-error-no-babel-config-file-detected-when-ide-not-open-at-vue-projects
- https://note.com/takoyaki_shikibu/n/ne2f3b802e08c