LoginSignup
499
479

More than 3 years have passed since last update.

Nuxt.js+Firebaseの認証・認可を実装した雛形プロジェクトを公開しました

Last updated at Posted at 2020-06-08

この記事について

NuxtとFirebaseを使って、これまでいくつかサービス開発をしていますが、認証/認可の実装はどのサービスでも毎回同じようなコードを書いている気がします。

サービスとしてのコア部分ではないですが、センシティブな部分なのでしっかりと調べながら実装すると結構大変ですよね(毎回時間がかかってしまいます)。

自分だけではなく、いろんな人が同じような課題感を感じているのではないかと思い、これから何かサービスを開発する人のためのNuxt+Firebaseのスターター用のプロジェクトを作成し、テンプレート化して使いまわせるようにリポジトリを公開しました。今回はそのリポジトリの概要を解説します。

※ 自分の考えるベストプラクティスなので、もっとこうした方がいいよね、これヤバそう...などのご意見やマサカリ、プルリク、フィードバック歓迎しております🙏

対象

  • NuxtとFirebaseで何か開発しようと考えている人
  • 認証認可のあるサービスをゼロから作ろうとしている人
  • create-nuxt-appではない、サンプルのプロジェクトを動かしてみたい人

※ Firebaseプロジェクトをゼロから作成する部分から解説するので、必要ない人は飛ばしてください🙏

Githubのリポジトリ

下記のリポジトリをクローンしてください。

nuxt-firebase-project
https://github.com/FujiyamaYuta/nuxt-firebase-project.git

技術

  • Nuxt.js
  • Buefy + Bulma
  • Firebase🔥
    • Hosting
    • Cloud Firestore
    • Cloud Storage
    • Authentication

環境

% firebase --version
8.4.0

 % npm -v
6.14.4

Firebaseの設定

今回はFirebaseの以下のサービスを使います(ある一定の転送量までは全て無料で使うことができます🙏)。それぞれがどのようなサービスかは、別で調べてみてください。

  • Hosting
  • Cloud Firestore
  • Cloud Storage
  • Authentication

はじめにFirebaseの設定をします。以下のリンクからプロジェクトを作成してください。

Firebase - プロジェクトの追加
https://console.firebase.google.com/u/0/?hl=ja

① プロジェクトを作る

①.png

② プロジェクト名を決める

②.png

③ プロジェクトIDとウェブAPIキーを確認

プロジェクト作成後の 「プロジェクトの管理」 の右の設定アイコンを押すとSettingsのページに遷移するのでそこから確認することができます。プロジェクトIDウェブAPIキーはNuxt側に設定する必要があるのでメモしておいてください。

③.png

④ AuthticationのGoogleの認証を許可する

Authticationのバーをクリックして Sign-in method のタブをクリックして、Authticationで認証を許可するサービスを選択します。今回はGoogleの認証を使うので、以下の手順で許可してください。

④.png

Authticationでは、認証できるホストを管理することが可能です。カスタムドメインで認証を許可したい場合などは「ドメインを追加」から、追加することができます。

GithubとTwitterで同じメールアドレスを使用しているユーザーの認証を許可したい場合、「1つのメールアドレスにつき複数のアカウント」に変更することで、同じメールアドレスでも複数のプロバイダからログインすることが可能になります。デフォルトは「1つのメールアドレスにつき1つのアカウント」なので、同じメールアドレスで認証は失敗します。

スクリーンショット 2020-06-09 7.30.25.png

⑤ Cloud Firestoreをプロビジョニング

Databaseのバーをクリックして、Firestoreを方を選択してください。(RealTimeDatabaseもあるので注意)
リージョンを選択できるので asia-northeast1(東京) を選択することをお勧めします。海外のリージョンを選択すると、物理的な距離が遠いのでレイテンシが発生するため。

⑤.png

⑥ Cloud Storageをプロビジョニング

こちらもリージョンを選択できるので asia-northeast1(東京) を選択することをお勧めします。

⑥.png

Firebase側の設定は以上になります。

Nuxtの設定

Githubのリポジトリからクローンした src/plugins/firebase.js のファイルにFirebaseプロジェクトで設定されたプロジェクトIDとウェブAPIキーを追加します。

src/plugins/firebase.js
import firebase from 'firebase';

// ** Firebaseプロジェクトの設定を記す
if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: '{ウェブAPIキー}',
    authDomain: '{プロジェクトID}.firebaseapp.com',
    databaseURL: 'https://{プロジェクトID}.firebaseio.com',
    projectId: '{プロジェクトID}',
    storageBucket: '{プロジェクトID}.appspot.com',
    messagingSenderId: '1234567890' // ** cloudmessagingを使う場合は設定
  })
}

export const firestore = firebase.firestore();
export const storage = firebase.storage();

Nuxt側の設定は以上になります。

ローカルホストで動作確認

// モジュールをインストール&ビルド
% npm install
% npm run build
% npm run dev 

localhost:3000でブラウザからアプリケーションが立ち上がるのを確認します。

スクリーンショット 2020-06-08 12.17.39.png

とりあえず、ローカルでは動いています🙌

Cloud functionsにもモジュールをインストールしておきます。

% cd functions
% npm install

デプロイ

ビルドしたモジュールをデプロイします。
※firebase CLIでログインしていない方は firebase loginを実行してください。

// 先ほど作ったFirebaseプロジェクトが存在するか確認
% firebase projects:list

ちゃんとありますね👍

⑧.png

先ほど作ったFirebaseプロジェクトにデプロイします。

% firebase use {プロジェクトID}
Now using project {プロジェクトID}

% firebase deploy

※ 下記のエラーは発生した場合は functions のディレクトリで npm install のコマンドを実行してください。

Error: Error parsing triggers: Cannot find module 'firebase-functions'
Require stack:

これで、URLからアクセスができたはずです🙌 おつかれさまでした!Google認証をすると、認証結果の情報がFirestoreに登録されています。

※ Github、Twitter、Facebookについても、Firebaseと連携すれば認証が使えるようになります。
スクリーンショット 2020-06-08 12.44.35.png

スクリーンショット 2020-06-08 12.45.40.png

動くものがデプロイでき、動作確認ができたのでNuxtの説明をします。

プロジェクトの紹介

クローンしたプロジェクトは以下のようになっています。 .eslintrc.jsnuxt.config.js は自分標準のファイルを入れいているので、個人のお好みでカスタマイズしてください。

src配下に開発者用のソースが格納されています。

.
├── README.md
├── database.rules.json
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
├── jsconfig.json
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── public
├── src
│   ├── assets
│   ├── components
│   ├── layouts
│   ├── middleware
│   ├── pages
│   ├── plugins
│   ├── static
│   └── store
└── storage.rules

Authentication

① 認証状態の永続性をセット
Firebaseでは認証状態をどの程度維持するかを指定することが可能です。
https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ja

列挙型 説明
firebase.auth.Auth.Persistence.LOCAL 'local' ブラウザ ウィンドウを閉じたり React Native でアクティビティが破棄されたりした場合でも、状態が維持されることを示します。この状態をクリアするには、明示的なログアウトが必要です。Firebase Auth のウェブ セッションは単一のホストを生成元とするため、単一のドメインでのみ永続化されることに注意してください。
firebase.auth.Auth.Persistence.SESSION 'session' 現在のセッションまたはタブでのみ状態が維持され、ユーザーが認証を受けたタブやウィンドウを閉じるとクリアされることを示します。ウェブ アプリケーションのみに適用されます。
firebase.auth.Auth.Persistence.NONE 'none' 状態はメモリにのみ保存され、ウィンドウまたはアクティビティが更新されるとクリアされることを示します。

イメージとしては永続性をローカルストレージ/ セッションストレージ / メモリのどこに保持するか?という認識でよいを思います(Webの場合はデフォルトlocal)。GoogleAuthProvider() の関数を呼び出す前に、永続性の明示的に呼び出します。


firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION)
  .then(function() {
    return firebase.auth.GithubAuthProvider()
  })
  .catch(function(error) {
    // Handle Errors here.
    let errorCode = error.code;
    let errorMessage = error.message;
  });

Firebaseの関数はほとんどか非同期処理になっているので、①をした後に②をして③をして④をする...のような同期処理にしたい場合はPromiseやasync/awaitを使う必要がありそうですね。コールバックの後に続けて書いてもいいですが、ネストが深くなりコードが読みにくくなるので、あまりオススメではありません。

いい感じに関数化して、順番に呼び出すような書き方をしてみました。

LoginModal.vue
<template>
  <b-modal :active.sync="isLoginModalActive" :width="420" scroll="keep">
    <button @click="google">
      <span class="icon">
        <i class="fab fa-google"></i>
        &nbsp;
        <span>Google</span>
      </span>
    </button>
  </b-modal>
</template>

<script>
import "bulma/css/bulma.min.css";
import "@fortawesome/fontawesome-free/css/all.min.css";
import "bulma-social/bin/bulma-social.min.css";

import firebase from "firebase";
import { firestore, storage } from "~/plugins/firebase.js";

export default {
  data() {
    return {
      isLoginModalActive: true
    };
  },
  methods: {
    // ** Google認証を行うときに呼び出される関数
    google() {
      // ** ② Google認証
      const auth = () => {
        return new Promise((resolve, reject) => {
          const authUI = new firebase.auth.GoogleAuthProvider();
          console.log("auth");
          // This gives you a the Google OAuth 1.0 Access Token and Secret.
          firebase
            .auth()
            .signInWithPopup(authUI)
            .then(result => {
              resolve(result);
            })
            .catch(error => {
              // Handle Errors here.
              const errorCode = error.code;
              const errorMessage = error.message;
              const email = error.email;
              const credential = error.credential;
              reject(error);
            });
        });
      };

      // ** ③ 認証後のユーザー情報を取得してオブジェクト化
      const getAccountData = result => {
        return new Promise((resolve, reject) => {
          let userObject = {};
          let user = result.user;
          userObject.token = result.credential.accessToken;
          userObject.refreshToken = user.refreshToken;
          userObject.uid = user.uid;
          userObject.displayName = user.displayName;
          userObject.photoURL = user.photoURL;
          userObject.uid = user.uid;
          userObject.email = user.email;
          userObject.isNewUser = result.additionalUserInfo.isNewUser;
          userObject.providerId = result.additionalUserInfo.providerId;
          resolve(userObject);
        });
      };

      // ** 同期的に順番に処理を実行する
      Promise.resolve()
        .then(this.setPersistence)
        .then(auth)
        .then(getAccountData)
        .then(userObject => this.createPhotoURL(userObject))
        .then(userObject => this.setPublicUserData(userObject))
        .then(userObject => this.setPrivateUserData(userObject))
        .then(userObject => this.setLocalUserData(userObject))
        .catch(error => this.onRejectted(error));
    },
    // ** ① 認証状態を明示的にセットする
    setPersistence() {
      return new Promise((resolve, reject) => {
        firebase
          .auth()
          .setPersistence(firebase.auth.Auth.Persistence.LOCAL)
          .then(result => {
            resolve();
          });
      });
    },
    // ** ④ Googleから取得したアイコンのURLをFirestorageに登録して、そのURLをFirestoreに登録する準備
    createPhotoURL(userObject) {
      return new Promise((resolve, reject) => {
        // ** TODO - 初めてじゃない場合は処理しない対応が必要
        let url = userObject.photoURL;
        let xhr = new XMLHttpRequest();
        xhr.responseType = "blob";
        xhr.onload = function(event) {
          let blob = xhr.response;
          let storageRef = storage.ref();
          let mountainsRef = storageRef.child(
            `user/${userObject.uid}/image.jpg`
          );
          let uploadTask = mountainsRef.put(blob);
          uploadTask.then(snapshot => {
            uploadTask.snapshot.ref.getDownloadURL().then(downloadURL => {
              console.log(downloadURL);
              // ** firestorageに登録したURLを登録するオブジェクトに代入
              userObject.photoURL = downloadURL;
              resolve(userObject);
            });
          });
        };
        xhr.open("GET", url);
        xhr.onerror = function(e) {
          // クロスドメインでひっかかる場合はstorageに登録しない
          console.log("ooooooops!!cros!!");
          resolve(userObject);
        };
        xhr.send();
      });
    },
    // ** ⑤ 公開可能なユーザー情報をFirestoreに登録
    setPublicUserData(userObject) {
      return new Promise((resolve, reject) => {
        let publicUser = firestore.collection("users").doc(userObject.uid);
        publicUser
          .set(this.createPublicObj(userObject), { merge: true })
          .then(result => {
            resolve(userObject);
          });
      });
    },
    createPublicObj(obj) {
      let publicObj = {};
      publicObj.uid = obj.uid;
      publicObj.providerId = obj.providerId;
      publicObj.isNewUser = obj.isNewUser;
      if (obj.isNewUser) {
        publicObj.photoURL = obj.photoURL;
        publicObj.displayName = obj.displayName;
      }
      if (
        (obj.providerId.indexOf("twitter") != -1 ||
          obj.providerId.indexOf("github") != -1) &&
        obj.isNewUser
      ) {
        // ** プロフィールが存在して、isNewUserがtrueじゃないときにオブジェクトに代入する
        publicObj.profile = obj.profile;
        publicObj.screenName = obj.screenName;
      }
      return publicObj;
    },
    // ** ⑥ 非公開のユーザー情報をFirestoreに登録
    setPrivateUserData(userObject) {
      return new Promise((resolve, reject) => {
        let privateUsers = firestore
          .collection("privateUsers")
          .doc(userObject.uid);
        privateUsers
          .set(this.createPrivateObj(userObject), { merge: true })
          .then(result => {
            resolve(userObject);
          });
      });
    },
    createPrivateObj(obj) {
      let privateObj = {};
      privateObj.uid = obj.uid;
      privateObj.providerId = obj.providerId;
      privateObj.isNewUser = obj.isNewUser;
      privateObj.email = obj.email;
      privateObj.token = obj.token;
      privateObj.refreshToken = obj.refreshToken;
      return privateObj;
    },
    // ** ⑦ ローカルストレージに保持するユーザー情報を設定
    setLocalUserData(userObject) {
      return new Promise((resolve, reject) => {
        let user = firestore.collection("users").doc(userObject.uid);
        user
          .get()
          .then(doc => {
            if (doc.exists) {
              localStorage.setItem("photoURL", doc.data().photoURL);
              localStorage.setItem("uid", userObject.uid);
              localStorage.setItem("token", userObject.token);
              localStorage.setItem("displayName", doc.data().displayName);
              this.$buefy.toast.open({
                duration: 5000,
                message: `ログインに成功しました`,
                position: "is-bottom",
                type: "is-success"
              });
              this.isLoginModalActive = false;
              location.reload();
              resolve(userObject);
            }
          })
          .catch(error => {
            console.log("Error getting document:", error);
          });
      });
    },
    // ** エラー処理
    onRejectted(error) {
      this.$buefy.toast.open({
        duration: 5000,
        message: `ログインに失敗しました。`,
        position: "is-bottom",
        type: "is-danger"
      });
      this.isLoginModalActive = false;
      console.log("onRejectted", error);
    }
  }
};
</script>

上の処理を実行することで、FirestoreとFirestorageにデータが登録されます🙌 コードは一部になりますが、LoginModal.vueというファイルをチェックしてみてください。

スクリーンショット 2020-06-08 12.24.32.png

スクリーンショット 2020-06-08 12.24.50.png

認証済みユーザーかどうかの確認

submitしたユーザーが本当に認証したユーザーかどうかを、データ登録前に確認したいことがあると思います。そのような場合には、 firebase.auth().onAuthStateChanged()の関数を呼び出すことで、確認することができます。コールバックに認証済みのユーザー情報が付加されています。

src/plugins/commonModule.js
isCommonLoginUser() {
  return new Promise((resolve, reject) => {
    firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        // ** ログイン済のユーザー
        console.log('ok!!Login User!!')
        var name, email, photoUrl, uid, emailVerified
        name = user.displayName
        email = user.email
        photoUrl = user.photoURL
        emailVerified = user.emailVerified
        uid = user.uid
        resolve(user)
      } else {
        // ** ログインしていないユーザーもしくは認証が切れている
        resolve(false)
      }
    })
  })
},

async submit() {
    try {
        let result = await this.isCommonLoginUser()
        // ** ↓↓ resultはuser or false ↓↓
    } catch (error) {
        console.log(error)
    }
},

セキュリティルールでも制御はしていますが、念のためチェックするとより安全かもしれません。

セキュリティルール

Firebaseの特徴はクライアントサイドから、データを読み書きするため、悪意あるユーザーが不正をしてくる恐れがあるためセキュリティルールはしっかりと書きましょう。

今回はルートにあるfirestore.rulesstorage.rulesというファイルにあらかじめセキュリティルールを追加しているため firebase deployのコマンドを実行すると、自動的にセキュリティルールが反映されます。

【Cloud Firestore セキュリティ ルールの条件の記述】
https://firebase.google.com/docs/firestore/security/rules-conditions?hl=ja

firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid}/{allPaths=**} {
            allow read;
            allow create: if request.auth != null;
            allow update,delete: if request.auth.uid == uid;
    }
    match /privateUsers/{uid}/{allPaths=**} {
            allow create: if request.auth != null;
            allow read,update,delete: if request.auth.uid == uid;
    }
  }
}
storage.rules
service firebase.storage {
  match /b/{bucket}/o {
    match /user/{uid}/{allPaths=**} {
      allow read;
            allow create: if request.auth != null;
            allow delete,update: if request.auth.uid == uid;
    }
  }
}

Firestoreにデータが登録されているか確認

最後に本当にユーザー情報以外のデータがFirestoreに登録されるかどうか確認してみましょう。
users/{uid}/rooms/{roomId}/** にデータが登録されているはずです🙌

スクリーンショット 2020-06-08 17.48.52.png

スクリーンショット 2020-06-08 17.49.21.png

終わりに

Nuxt+Firebaseでスターター用のプロジェクトを作ってみました。何かこれからサービスを作ろうと考えるときに、参考にするリポジトリのひとつになればと思います🙏

もっとこうした方がいいよね、これヤバそう...などのご意見やマサカリ、プルリク、フィードバックがありましたら、ぜひお願いいたします。

nuxt-firebase-project
https://github.com/FujiyamaYuta/nuxt-firebase-project.git

499
479
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
499
479