Firebase Authenticationを使用したVue Webアプリのログイン機能実装

## 概要



現在実装を進めている[個人開発のWebアプリプロジェクト](https://note.com/npower_creative/n/n48dc5182423b)について、備忘を含めた記事を進めています。
今回の記事は本アプリで必須となるログイン管理機能について、「Firebase Authentication」を用いた実装方法を記載していきます。

### 開発環境

- Vue 2.x
- Vuex / Babel / Vue-CLI
- ESLint + Prettier


### 参考文献

- [Firebase公式](https://firebase.google.com/?hl=ja)
- [Firebase を JavaScript プロジェクトに追加する](https://firebase.google.com/docs/web/setup?hl=ja)
- [これからFirebaseでプロジェクトを始めようとする全ての人が知っておくべきこと v8→v9リリース](https://zenn.dev/hiro__dev/articles/605161cd5a7875)
- https://qiita.com/suzuki-navi/items/a613bc47fccff091374f


## 事前のセットアップ

前提として、下記セットアップをしてあります。

- [Firebase コンソール](https://console.firebase.google.com/u/0/?hl=ja)への登録
- Firebaseプロジェクトを作成
- プロジェクト内でアプリを登録済み（Webアプリ）

## Firebaseでの作業

プロジェクトを作った後、該当プロジェクトのダッシュボードページを参照します。
![スクリーンショット 2022-03-18 10.26.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76115/4b9e89e0-ae7c-d0fa-320f-e8ab802d4757.png)

「Authentication」「Sign-in method」から各種プロバイダを使用したログイン方法の設定をする事ができます。今回はシンプルな「メール/パスワード」での設定方法を試してみます。

![スクリーンショット 2022-03-18 10.30.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76115/78b90482-55f9-56be-4a94-56fb2bf11c88.png)
「メール/パスワード」を選択するとポップアップが表示されるので、「有効にする」を選択して保存します。

![スクリーンショット 2022-03-18 10.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76115/b96844c8-53f3-9b9b-e93d-a7c0c8addf22.png)
続いて「Users」タブでユーザーを追加します。
「ユーザーを追加」ボタンを押下して表示されるポップアップから、任意のメールアドレス+パスワードを入力して追加します。

![31b678af6ad826470e05f08ec5f8bfbe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76115/548554a6-01b6-357b-2597-58c00df679ac.png)
メールアドレスがID、任意の文字列がユーザーUIDとして登録されます。


## Vueプロジェクト側の設定（Firebase連携）

プロジェクトを作った後、該当プロジェクトのダッシュボードページを参照します。
まずFirebaseとの連携について独立ファイルで定義していきます。

```javascript:src/firebase.js
import firebase from "firebase/compat/app";
import "firebase/compat/firestore";
import "firebase/compat/auth";
import "firebase/compat/storage";

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
  measurementId: "",
};

// init firebase
firebase.initializeApp(firebaseConfig); // バックエンドのfirebaseを初期化する

// init firestore service
const projectFirestore = firebase.firestore(); // firebaseのサービスも初期化する
const projectAuth = firebase.auth();
const projectStorage = firebase.storage();

const timestamp = firebase.firestore.FieldValue.serverTimestamp; //firebaseのtimestamp

export { projectFirestore, projectAuth, projectStorage, timestamp };

export default firebase;

```

公式サイトで記載されている方法をそのまま試したところエラーが発生していたのですが、これはバージョン違いによるものだと調べて判明したため、今後使う可能性のある他サービスも含め最終的に上記のような記述になっています。

なお、`firebaseConfig`にはFirebase側で発行された各値をコピペします。


## Vueプロジェクト側の設定（画面コンポーネント・共通コンポーネント）

次に画面側の記述です。
上記のFirebase連携ファイルを元に、必要に応じてインポートする流れになります。

```vue:src/views/Login.vue
<template>
  <div class="login l-login__container u-height--full">
    <h1>Logo</h1>
    <div class="l-login__contents">
      <div class="l-login__section u-mb20">
        <v-container>
          <v-row class="align-center">
            <v-col cols="12" md="4"> ユーザーID </v-col>
            <v-col cols="12" md="8">
              <v-text-field
                v-model="email"
                :rules="emailRules"
                label="email"
                required
              ></v-text-field>
            </v-col>
          </v-row>

          <v-row class="align-center">
            <v-col cols="12" md="4"> パスワード </v-col>
            <v-col cols="12" md="8">
              <v-text-field
                v-model="password"
                :rules="passwordRules"
                type="password"
                label="password"
                required
              ></v-text-field>
            </v-col>
          </v-row>

          <v-row class="align-center">
            <v-col cols="12" md="12">
              <v-btn class="mx-1" color="primary" @click="userSingIn">
                ログイン
              </v-btn>
            </v-col>
          </v-row>
        </v-container>
      </div>
      <div class="l-login__section">
        <div><h2>他のアカウントでログイン</h2></div>
        <div>Form</div>
      </div>
    </div>
  </div>
</template>

<script>
import firebase from "@/firebase";

export default {
  name: "Login",

  data: () => ({
    valid: false,
    // firstname: "",

    email: "",
    emailRules: [
      (v) => !!v || "email is required",
      // (v) => v.length <= 10 || "email must be less than 10 characters",
    ],
    password: "",
    passwordRules: [
      (v) => !!v || "password is required",
      // (v) => /.+@.+/.test(v) || "password must be valid",
    ],
  }),

  methods: {
    userSingIn() {
      firebase
        .auth()
        .signInWithEmailAndPassword(this.email, this.password)
        .then(() => {
          console.log("login success!");
          this.$router.push("/dashboard");
        })
        .catch(function (error) {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;

          console.log(error);
          console.log(errorCode);
          console.log(errorMessage);
        });
    },
  },
};
</script>


```

まずログイン画面のソースです。
先ほど定義した`firebase`ファイルをscript内でインポートし、ログインボタンを押した際の`userSingIn`メソッドでfirebase側でのサインイン処理を行なった後、ダッシュボード画面へリダイレクトされる流れになります。
なお各フォーム項目へはVuetifyベースでのバリデーションを実装しています。


```vue:src/views/Dashboard.vue
<template>
  <div class="dashboard">
    <Header />

    <div class="l-container">
      <SideMenu />

      <!-- メインコンテンツ -->
      
    </div>
  </div>
</template>

<script>
// Firebase認証
import firebase from "@/firebase";

// コンポーネント
import Header from "@/components/Header";
import SideMenu from "@/components/SideMenu";

export default {
  name: "Dashboard",
  props: [],

  components: {
    Header,
    SideMenu,
  },  

  mounted() {
    // ログインチェック
    firebase.auth().onAuthStateChanged(function (user) {
      if (user) {
        console.log("login");
        console.log(user);
      } else {
        console.log("logout");
        this.$router.push("/login");
      }
    });
  },

};
</script>

```

ログイン後の基盤となるダッシュボード画面では、
- 共通コンポーネント（ヘッダー、サイドメニュー）をインポート
- ログイン状態の管理を`mounted`で管理
という状態にします。
これは基本的に他の画面コンポーネントでも同様です。

ログイン状態が継続している場合は処理をせず、何らかの原因でログイン状態が外れている場合はログインページへのリダイレクトを行うという処理になります。

```vue:src/components/Header.vue
<template>
  <!-- ヘッダーメニュー -->
  <header class="l-header">
    <div class="l-header_contents">
      <div class="l-header_contentsTitle">Logo</div>
      <div class="l-header_contents_menuArea">
        <ul class="l-header_contents_menu">
          <li>[アカウントID]様</li>
          <li>
            <v-btn text to="/mypage">マイページ</v-btn>
          </li>
          <li>
            <v-btn text to="/mypage/edit_profile"> プロフィール変更 </v-btn>
          </li>
          <li>
            <v-btn outlined @click="logout">ログアウト</v-btn>
          </li>
        </ul>
      </div>
    </div>
  </header>
</template>

<script>
import firebase from "@/firebase";

export default {
  name: "Header",
  props: [],

  data() {
    return {};
  },

  mounted() {
    // ログインチェック
    firebase.auth().onAuthStateChanged(function (user) {
      if (user) {
        console.log("login");
        console.log(user);
      } else {
        console.log("logout");
        this.$router.push("/login");
      }
    });
  },

  methods: {
    logout() {
      firebase.auth().signOut();
      console.log("logout complete");
      this.$router.push("/login");
    },
  },
};
</script>

```

（サイドメニューではログイン機能を管理していないため省略します）
ログイン/ログアウト処理についてはヘッダコンポーネントで行っています。
ダッシュボードと同じく`firebase`ファイルをインポートし、`mounted`についてもほぼ同様です。
ログアウトボタンを押した際に`@click`メソッドを実装し、`logout`メソッドを通じてFirebase側でのログアウト（サインアウト）を行います。
そのままログイン画面へリダイレクトされる流れになります。