【AWS Amplify × Vue.js 簡単サーバーレスアプリ構築チュートリアル】③ Authでユーザー登録、ログイン機能実装編

# はじめに
この記事は、全部で6記事にわたるたチュートリアルの3つ目です。

[① 概要説明編](https://qiita.com/minato-naka/items/7b445bcf0794189e65a0)
[② Amplify利用準備・初期設定編](https://qiita.com/minato-naka/items/8e5c0999867d170a5f66)
[③ Authでユーザー登録、ログイン機能実装編](https://qiita.com/minato-naka/items/4e4dcfea2bf5231f6c86)
↑↑↑今ここ↑↑↑
[④ API（Graphql）でCRUD実装編](https://qiita.com/minato-naka/items/03da3987d5a81dfe865f)
[⑤ StorageでS3に画像保存実装編](https://qiita.com/minato-naka/items/abffc5e1e949a6fea494)
[⑥ Hostingでアプリ公開＆自動デプロイ実装編](https://qiita.com/minato-naka/items/3123950aa852ee3250fe)

前回の記事では、Vue.jsで表面上だけのアプリを構築し、
Amplifyを利用するための準備・初期設定などを行いました。

Amplify機能をアプリに組み込んでいく準備は整ったので、
今回はまずAuth機能から組み込んでいきます。

Auth機能とは具体的に言うと、
よくある認証系の下記の機能セットのことです。
・サインアップ
・サインイン
・サインアウト
・パスワードリセット

Amplify利用準備・初期設定編の[Amplify機能の利用手順](https://qiita.com/minato-naka/items/8e5c0999867d170a5f66#amplify%E6%A9%9F%E8%83%BD%E3%81%AE%E5%88%A9%E7%94%A8%E6%89%8B%E9%A0%86)で説明した通り、
①AmplifyCLIのaddでバックエンド設定追加
②AmplifyCLIのpushでバックエンドリソース作成
③フロントエンドにバックエンド連携コード実装
の3ステップでAuth機能を追加していきます。

# AmplifyCLIのaddでバックエンド設定追加

ターミナルで下記コマンドを実行します。
`amplify add auth`

質問が表示されるのでそれぞれ下記のように答えます。

`Do you want to use the default authentication and security configuration?` → `Default configuration`
`How do you want users to be able to sign in?` → `Username`
`Do you want to configure advanced settings?` → `No, I am done.`

その後下記のメッセージが表示されればステップ①完了です。

```shell
Successfully added auth resource amplifyvuealbumxxxxxxx locally
```

この様に、バックエンドの設定ファイルがいろいろ作成されています。
[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/6fda46aeb6e7b1f90be613ac6b9decc5368d0fe8)

# AmplifyCLIのpushでバックエンドリソース作成

次に、このバックエンド設定で実際にクラウド上に
AWSリソースを作成します。

ターミナルで下記コマンドを実行します。
`amplify push`

すると下記のように確認が表示されるのでYesにします。

```shell
| Category | Resource name           | Operation | Provider plugin   |
| -------- | ----------------------- | --------- | ----------------- |
| Auth     | amplifyvuealbum921b97fd | Create    | awscloudformation |
? Are you sure you want to continue? Yes
```

その後AWSリソースの作成処理がしばらく実行されます。

最後にこのメッセージが表示されれば完了です。

```shell
√ All resources are updated in the cloud
```

AWSのCognitoの画面に、
新しくユーザープールが追加されているのを確認できると思います。
（画面右上のリージョン選択を「us-east-1」にしてください）

# フロントエンドにバックエンド連携コード実装

バックエンドにCognitoは作成できたので、
次にこの機能を利用するためのコードをフロントエンドに書いていきます。

フロントエンドコードは
・AuthUI実装
・Auth状態保持処理実装
・アクセス制御実装
という段階に分けて実装をしていきます。

## AuthUI実装
AuthUI実装とは、Auth機能の画面の見た目を実装することです。
今回は [@aws-amplify/ui-components](https://docs.amplify.aws/ui/q/framework/vue) というライブラリを利用するため、
UIはほとんどライブラリが描画してくれます。

SignInのコンポーネントに、このように記述します。

```diff:src/views/SignIn.vue
  <template>
    <div>
      <h1>サインイン</h1>
+     <amplify-authenticator></amplify-authenticator>
    </div>
  </template>
```

また、App.vueには全ページ共通でサインアウトボタンを表示するため
この様に記述します。

```diff:src/App.vue
  <template>
+   <amplify-greetings></amplify-greetings>
    <router-view/>
  </template>
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/70dfba2c8544839c785c5e3518f766871542aef1)


実は、この1行ずつの追加だけで、
・サインアップ
・サインアップ確認
・サインイン
・パスワードリセット
・サインアウト
の機能を導入完了しています。

これでサインインページにアクセスしてみましょう。
http://localhost:8080/signin

この様な画面が表示されると思います。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297693/6f7db6a8-bf27-424a-77f2-78798d36b431.png)

軽く動かしてみましょう。
フォーム下部にある「Create account」リンクをクリックしてサインアップ画面を表示します。
フォームに任意の入力をして「CREATE ACCOUNT」をクリックします。

すると、「Confirm Sign up」という画面に切り替わります。
先ほど入力したメールアドレスに、
確認コードが記載されたメールが送信されているはずです。

その確認コードを画面に入力し、
「CONFIRM」をクリックすればユーザー登録完了です。

実際にAWSのCognitoにユーザーデータが登録されているので、
確認してみましょう。
AWSのCognitoページを開き、自分のユーザープールにアクセスします。
「ユーザーとグループ」というメニューにアクセスすると、
現在登録されているユーザーが一覧で表示され、
先ほど自分が登録したユーザーが表示されていると思います。

「SIGN OUT」ボタンをクリックすればサインアウトされ、
サインインページで先ほど登録した
UsernameとPasswordを入力すればサインインできる状態です。

## Auth状態保持処理実装

ライブラリのUIコンポーネントを利用して
一通りのAuth機能UIを実装しましたが、
この状態でサインインやサインアウト処理を実行しても
Cognito側で「認証済み」「未認証」の判定をして、
Cognito側で「認証済み」か「未認証」かの状態を保持しているだけです。

次は、そのCognitoの認証判定結果を
自分のアプリ側に保存し、
「認証済み」なのか
「未認証」なのか
の状態を保持する、
という処理を追加する必要があります。

まずはstoreにユーザー情報を保存する
userというステートを追加します。

```diff:src/store/index.js
  export default createStore({
    state: {
+     user: null,
    },
    mutations: {
+     setUser(state, user) {
+       state.user = user;
+     },
    },
    actions: {
    },
    modules: {
    }
  })

```

ユーザーがサインイン済みであればこのuserステートに情報を保存し、
ユーザーがサインインしてなければこのuserステートを空にします。

その処理を、routerに書いていきます。

```diff:src/router/index.js
  import { createRouter, createWebHistory } from "vue-router";
+ import store from "@/store/index.js";
+ import Auth from "@aws-amplify/auth";
  import SignIn from "../views/SignIn.vue";
  import AlbumIndex from "../views/album/Index.vue";
  import AlbumCreate from "../views/album/Create.vue";

  // 省略

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


+ function getAuthenticatedUser() {
+   return Auth.currentAuthenticatedUser()
+     .then((data) => {
+       if (data && data.signInUserSession) {
+         store.commit("setUser", data);
+         return data;
+       }
+     })
+     .catch((e) => {
+       console.error(e);
+       store.commit("setUser", null);
+       return null;
+     });
+ }
 

+ router.beforeResolve(async (to, from, next) => {
+   await getAuthenticatedUser();
+
+   return next();
+ });

  export default router;
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/2c4f5f0de7b7441bbd0d6323ffc6f0dd80bbe5e7)

`getAuthenticatedUser()` と `router.beforeResolve`
の2つの処理を追加しています。

`getAuthenticatedUser()` は `Auth.currentAuthenticatedUser()` という
Amplifyのライブラリが提供しているメソッドを使い、
・現在ユーザーが認証済みであればそのユーザー情報をuserステートに保存
・未認証であればuserステートを空にする
という処理を行っています。

そしてその下の `router.beforeResolve` は、
画面遷移するたびに実行される処理です。
画面遷移のたびに先ほどの `getAuthenticatedUser()` を呼び出して、
userステートの状態を更新しています。

これで、Cognito側だけで保持していたユーザーの認証状態を、
自分のアプリ側でも保持している状態になりました。

## アクセス制御実装
あとは、そのユーザーの認証状態によって、
アプリに何かしらの制御を入れていきます。

・認証状態によってアクセス可能なページ、アクセス不可能なページを制御する
・認証状態によって特定のページ要素の表示、非表示を制御する
といった制御を入れるのが典型的なものですね。

今回もそのような制御を入れていきます。

まずは、下記の制御を実装します。
・サインインページは未サインイン状態のユーザーのみアクセス可能にする
・それ以外のページはサインイン済みのユーザーのみアクセス可能にする

```diff:src/router/index.js
  // 省略
  const routes = [
    {
      path: "/",
      redirect: { name: "AlbumIndex" },
    },
    {
      path: "/signin",
      name: "SignIn",
      component: SignIn,
    },
    {
      path: "/albums",
      name: "AlbumIndex",
      component: AlbumIndex,
+     meta: { requireAuth: true },
    },
    {
      path: "/albums/create",
      name: "AlbumCreate",
      component: AlbumCreate,
+     meta: { requireAuth: true },
    },
    {
      path: "/albums/:albumId/edit",
      name: "AlbumEdit",
      component: AlbumEdit,
      props: true,
+     meta: { requireAuth: true },
    },
    {
      path: "/albums/:albumId",
      name: "AlbumShow",
      component: AlbumShow,
      props: true,
+     meta: { requireAuth: true },
    },
    {
      path: "/albums/:albumId/photo/create",
      name: "PhotoCreate",
      component: PhotoCreate,
      props: true,
+     meta: { requireAuth: true },
    },
  ];

  // 省略


+ let user;

  router.beforeResolve(async (to, from, next) => {
-   await getAuthenticatedUser();
+   user = await getAuthenticatedUser();
+
+   if (to.name === "SignIn" && user) {
+     return next({ name: "AlbumIndex" });
+   }
+
+   if (to.matched.some((record) => record.meta.requireAuth) && !user) {
+     return next({ name: "SignIn" });
+   }
    return next();
  });

  // 省略
```

[コミット](https://github.com/MinatoNaka/AmplifyVueAlbum/commit/5d52cef4c7667d59d363c072322a62a4d6f7fbd2)

サインインページ以外のルーティング定義に、
`meta: { requireAuth: true }` を追加しています。

そして `router.beforeResolve` の方では、
ユーザー認証状態を取得したうえで
・サインインページでユーザー認証済みの場合はアルバム一覧ページに遷移させる
・ `requireAuth: true` のページでユーザー未認証の場合はサインインページに遷移させる
という処理を入れています。

さらに、
・サインイン完了時にアルバム一覧ページにリダイレクトさせる
・サインアウト完了時にサインインページにリダイレクトさせる
という処理を入れておきましょう。

```diff:src/router/index.js
  import { createRouter, createWebHistory } from "vue-router";
  import store from "@/store/index.js";
  import Auth from "@aws-amplify/auth";
+ import { AuthState, onAuthUIStateChange } from "@aws-amplify/ui-components";
  import SignIn from "../views/SignIn.vue";
  import AlbumIndex from "../views/album/Index.vue";
  import AlbumCreate from "../views/album/Create.vue";

  // 省略


+ onAuthUIStateChange((authState, authData) => {
+   if (authState === AuthState.SignedIn && authData) {
+     router.push({ name: "AlbumIndex" });
+   }
+   if (authState === AuthState.SignedOut) {
+     router.push({ name: "SignIn" });
+   }
+ });

  export default router;
```

Amplifyライブラリの `onAuthUIStateChange` という機能を使っています。
このメソッドは、サインイン、サインアウト、サインアップなど、
Auth系の処理が実行されるたびに自動で呼び出されます。

そこで、
・サインイン完了したらアルバム一覧にリダイレクト
・サインアウト完了したらサインインページにリダイレクト
という処理を入れています。

これで、
認証状態によってアクセス可能なページ、アクセス不可能なページを
制御することができました。


次に、サインアウトボタンの表示制御を行いましょう。
いま、ユーザーが認証済み状態でも未認証状態でも、
いつでもサインアウトボタンが画面上部に表示されてしまっています。

これを、ユーザー認証済み状態の場合のみ、表示するように制御します。
サインアウトボタンを設置していたApp.vueを下記のように修正しましょう。

```diff:src/App.vue
  <template>
-   <amplify-greetings></amplify-greetings>
+     <div v-if="isSignedIn">
+       <amplify-greetings></amplify-greetings>
+     </div>
    <router-view/>
  </template>


+ <script>
+ export default {
+   computed: {
+     isSignedIn() {
+       return this.$store.state.user !== null;
+     },
+   },
+ };
+ </script>
```

先ほどstoreに追加したuserステートを参照し、
もしuserステートが存在すればサインアウトボタンを表示し、
空であればサインアウトボタンを非表示にしています。

# おわりに

これで、Amplifyを利用した基本のAuth機能は一通り実装完了です。
今回は一番シンプルな例だけ紹介しましたが、
・サインイン、サインアップフォームの項目を変更する
・Auth系UIの表示文、ラベル名、文字色、ボタン色などを変更する
・GoogleやFaceBookなど外部アプリを利用したAuth機能の導入
・サインアップ時の本人確認方式の変更
・UI表示文言の日本語化
など様々なカスタマイズが可能です。

そのうち別の記事で上記のカスタマイズ方法も解説しようかと思います。

次は、AmplifyのAPI（Graphql）を利用して基本のCRUD機能を実装していきます。
（次に進む前に、LGTMしてもらえるとうれしいです）
[【AWS Amplify × Vue.js 簡単サーバーレスアプリ構築チュートリアル】④ API（Graphql）でCRUD実装編](https://qiita.com/minato-naka/items/03da3987d5a81dfe865f)