Firebaseの認証処理とCloud Firestoreのセキュリティルール

前回の記事でCloud Firestoreの簡単な使い方を紹介しましたが、セキュリティルールの設定をしていないため、だれでもデータの操作ができる状態になっていました。
今回はこのCloud Firestoreのデータベースにセキュリティルールを設定し、サインインされているユーザーだけが書き込みできるようにします。サインインの認証は、Firebaseの認証サービスAuthenticationを利用します。

認証処理の準備

今回の認証方法は、メール・パスワードによるログインで行います。Firebaseのコンソール左メニューの「Authentication」→「ログイン方法」→「メール/パスワード」からメール・パスワードによる認証方法を有効にします。

ダイアログが表示されますので、メール/パスワードの設定を「有効にする」にして「保存」をおします。

次に、メールアカウントの登録を行います。通常のサービスであればユーザーが各々入力すると思いますが、ここではコンソールからメールとパスワードを追加して、テスト用のアカウントを作成します。  
ログインユーザー毎にドキュメントのを独立させたい&ログインの状況を確認したいので、アカウントは2つ作成します。
コンソールの「Authentication」→「ユーザー」→「ユーザーを追加」の順にクリックすると、メールアドレスとパスワードを入力するダイアログが表示されますので、適宜入力してユーザーを追加してください。  
この記事では以下のアカウントを作成します。  

  • ユーザー1:
    user_1@example.com / test1234
  • ユーザー2:
    user_2@example.com / test1234

ユーザーを追加したら以下のようになっているか確認してみてください。※ユーザーIDはことなります。

これで認証処理の準備が整いました。次はCloud Firestoreのセキュリティルールを設定しましょう。

Cloud Firestoreのセキュリティルールを設定

前回の記事ではテストモードでルールを設定していたため、だれでもデータを操作できるようになっていました。ここではログインしたユーザーが指定されたドキュメントだけを操作できるように、Cloud Firestoreのセキュリティルールを書き換えます。

まずは、コレクションを用意しておきます。今回はユーザー毎にメッセージを保存しておくので、messagesコレクションを作成し、空のデータを入れたドキュメント(自動で付与されるID)を入れておきます。

次に、セキュリティルールを設定します。「Database」→「ルール」からセキュリティルールを設定します。

テストモードだと次のようになっていると思います。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}

これをログインしているユーザー専用のドキュメントだけを操作できるようにするには、次のようにします。今回はmessagesコレクションを用意しているため、databasesにネストされているmatchの条件にはmessagesコレクションを指定して、ドキュメントは{userId}と指定します。

service cloud.firestore {
  match /databases/{database}/documents {
    //- コレクション/ドキュメント/コレクション/ドキュメント/・・・の順で、
    //- ルールを適用するコレクションorドキュメントを指定
    match /messages/{userId}{
      //- request.authで認証済みのユーザーオブジェクトが格納されているので、
      //- userIdドキュメントのIDと一致するユーザーID(uid)の場合だけ,
      //- read,writeを許可
        allow read, write: if request.auth.uid == userId;
    }
  }
}

ログインしているユーザー全員にデータを読ませるには次のようになります。
モックアップでサービスを作っているときなど使いそうです。
request.auth != null で 認証されていない(ログインしていない)場合はread(読み込み)を許可といった条件になります。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**}{
        allow read: if request.auth != null;
    }
  }
}

今回は、ログインしているユーザーが、自分のユーザーIDと同じIDのドキュメントだけを操作できるようにするため、以下のように設定します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /messages/{userId}{
        allow read, write: if request.auth.uid == userId;
    }
  }
}

これでセキュリティルールの設定が完了しました。
これからJavaScriptで認証処理やセキュリテイルールの確認をしていきます。

ソースコードの解説アジェンダ

基本的にHTMLにJavaScriptを記述していきます。コメントに番号を振っていますので、その順番でJavaScriptやHTMLを記述・解説していきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Google Firestore Lab</title>
</head>
<body>

  <!-- 認証処理-->
  <div id="auth" style="display: none;">
    <input id="email" type="email">
    <input id="pswd" type="password">
    <button id="loginBtn" type="button">ログイン</button>
  </div>
  <!-- ログイン後-->
  <div id="output" style="display: none;">
    <button id="logoutBtn" type="button">ログアウト</button>
    <p>ユーザーID : <span id="uid"></span></p>
    <input id="message" type="text" placeholder="メッセージを入力">
    <button id="saveBtn" type="button">保存</button>
  </div>
  <!-- firebase firestoreのインストール-->
  <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
  <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase-firestore.js"></script>
  <script>
    /*---------------------------------------*/
    /* 1.firebase初期化 */
    /*---------------------------------------*/
    const config = {
        apiKey: "ウェブAPIキー",
        authDomain: "オースドメイン",
        projectId: "プロジェクトID",
    }
    firebase.initializeApp(config);


    /*---------------------------------------*/
    /* 3.アプリの初期化 */
    /*---------------------------------------*/
    function initApp(){

        /*---------------------------------------*/
        /* 4.DOMセット */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 5.認証状態の監視 */
        /*---------------------------------------*/

            /*---------------------------------------*/
            /* 5.1 ログインしている場合 */
            /*---------------------------------------*/

                /*---------------------------------------*/
                /* 8.ユーザーIDと同名のドキュメントに書き込み */
                /*---------------------------------------*/

            /*---------------------------------------*/
            /* 5.2 ログインしていない場合 */
            /*---------------------------------------*/



        /*---------------------------------------*/
        /* 6.ログインの処理 */
        /*---------------------------------------*/

        /*---------------------------------------*/
        /* 7.ログアウトの処理 */
        /*---------------------------------------*/

    }
    /*---------------------------------------*/
    /* 2.DOMの読み込みが終わったら */
    /*---------------------------------------*/
    document.addEventListener('DOMContentLoaded',initApp);

  </script>
</body>
</html>

1.firebaseの初期化

firebaseの初期化で必要となる、「apiKey」「authDomain」「projectId」は、firebaseコンソールの「Overview」→「ウェブアプリにFirebaseを追加」から確認できます。

全てでも良いですし、適宜値をコピーしたら貼り付けて、initializeApp()でfirebaseオブジェクトを初期化します。

const config = {
    apiKey: "ウェブAPIキー",
    authDomain: "オースドメイン",
    projectId: "プロジェクトID",
}
firebase.initializeApp(config);

2,3 DOMの読み込み → アプリの初期化

ボタンやフィールドを操作するため、移行はDOMの読み込みが終わった後に、自作のアプリの初期化を実施します。

/*---------------------------------------*/
/* 3.アプリの初期化 */
/*---------------------------------------*/
function initApp(){
  //- DOMの読み込みが終わったら実行される

}
/*---------------------------------------*/
/* 2.DOMの読み込みが終わったら */
/*---------------------------------------*/
document.addEventListener('DOMContentLoaded',initApp);

4.DOMセット

ボタンをクリックしたり、フィールドの値を参照したりするため、予めDOMを変数にセットしておくことにします。

/*---------------------------------------*/
/* 4.DOMセット */
/*---------------------------------------*/
//- 認証性で使う部分
const $auth = document.querySelector('#auth');
const $email = document.querySelector('#email');
const $pswd = document.querySelector('#pswd');
const $loginBtn = document.querySelector('#loginBtn');

//- 認証が通った後に使う部分
const $output = document.querySelector('#output');
const $logoutBtn = document.querySelector('#logoutBtn');
const $uid = document.querySelector('#uid');
const $message = document.querySelector('#message');
const $saveBtn = document.querySelector('#saveBtn');

ここはHTMLタグと照らし合わせてご確認ください。

5.認証状態の監視

ページにアクセスしたユーザーが、firebaseに認証されているかどうかは、auth()オブジェクトのonAuthStateChanged()メソッドによって監視することが可能です。返り値としてユーザーオブジェクトが帰ってきますので、ここではuserに格納するようにしています。

/*---------------------------------------*/
/* 5.認証状態の監視 */
/*---------------------------------------*/
firebase.auth().onAuthStateChanged(user => {
    if(user){
    /*---------------------------------------*/
    /* 5.1 ログインしている場合 */
    /*---------------------------------------*/

    }else{
    /*---------------------------------------*/
    /* 5.2 ログインしていない場合 */
    /*---------------------------------------*/

    }
});

返り値であるuserに値が格納されていればログインしている時の処理、それ以外の場合はログインしていない時の処理をifで条件分岐します。 最初はログインされていないと思いますので、5.2ログインしていない場合の処理を記述します。

/*---------------------------------------*/
/* 5.2 ログインしていない場合 */
/*---------------------------------------*/
console.log('logged out');
$auth.style.display = 'block';
$output.style.display = 'none';

ログインしていない場合の処理は単純で、認証に関するHTMLは表示して、それ以外はそのまま非表示にするというだけです。サーバーサイドで出し入れした方が良いと思いますが、ここでは簡易的に再現しています。

ログインしている場合もほぼ同じですが、後でメッセージの書き込み等の処理を追加することになります。

/*---------------------------------------*/
/* 5.1 ログインしている場合 */
/*---------------------------------------*/
console.log(user);
$uid.innerHTML = user.uid;
$auth.style.display = 'none';
$output.style.display = 'block';

/*---------------------------------------*/
/* 8.ユーザーIDと同名のドキュメントに書き込み */
/*---------------------------------------*/
//- 後で追記

引数であるuserにユーザー情報が格納されていますので、ログにてご確認ください。跡でユーザーID(user.uid)をつかう事になります。

6.ログインの処理

さて、ステップ5までで認証状態の監視ができるようになりましたので、次は実際にログインの処理を実装します。ログインボタンが押されたらEmailフィールドとパスワードフィールドの値を参照し、auth()オブジェクトのsignInWithEmailAndPassword()メソッドの第一、二引数にそれぞれ代入します。

/*---------------------------------------*/
/* 6.ログインの処理 */
/*---------------------------------------*/
$loginBtn.addEventListener('click', event => {
    const email = $email.value;
    const pswd = $pswd.value;
    firebase.auth().signInWithEmailAndPassword(email,pswd).catch(error => {
        console.error(error);
    });
});

正常に認証されるとステップ5で認証状態の変化を監視しているため、5.1のログインしている場合の処理が実行されます。それ以外はコンソールにエラーが出力されます。
準備段階で作成したアカウント user_1@example.com と test1234 をそれぞれ入力して、実際にログインできるか確認してください。
※自分で追加したアカウントがある場合は、メールとパスワードを適宜置き換えてください。

7.ログアウトの処理

次はログアウトの処理です。ログアウトはauth()オブジェクトのsignOut()メソッドによって実装できます。

/*---------------------------------------*/
/* 7.ログアウトの処理 */
/*---------------------------------------*/
$logoutBtn.addEventListener('click', event => {
    firebase.auth().signOut();
});

ここまでで、ログイン・ログアウトの処理ができましたので、実際に動作確認をしてみます。

ログイン前:

ログイン後:

このようにログイン後にユーザーIDやログアウトボタンなどが表示されると正常です。
記事の最後にHTML全景を貼り付けておきますので、うまく動作しない場合はそちらをご参照ください。

8.ユーザーIDと同名のドキュメントに書き込み

最後に、ログインしたユーザーの書き込みテストをしてみます。現在のCloud Firestoreのセキュリティルールでは、ログインしたユーザーIDとドキュメントのIDが同じ場合しか書き込み、読み込みが許可されていません。データベースは次のようになります。

書き込み処理を実装することでドキュメントが追加されるため、現時点では上記画像のようになってはいませんが、ユーザーIDとドキュメントIDが一致するというイメージを持っていただけるでしょうか。
※エラーが出る場合はmessagesコレクションは事前に作成し、空のドキュメントを作ってみてください。

さて、ユーザーIDと同じドキュメントIDに書き込む処理は次の通りです。

/*---------------------------------------*/
/* 8.ユーザーIDと同名のドキュメントに書き込み */
/*---------------------------------------*/
//- firestoreインスタンス生成
const db = firebase.firestore();

//- messageコレクションのユーザーIDドキュメントを指定して接続
const docRef = db.collection('messages').doc(user.uid);

/*
ドキュメントへの接続は以下の記述でも可能
const docRef = db.doc('/messages/'+user.uid);
*/

//- イベントリスナー追加
$saveBtn.addEventListener('click', event => {
    docRef.set({
        value: $message.value
    }).then(() => {
        console.log('success');
    }).catch(error => {
        console.error(error);
    });
})

前回の記事 Cloud Firestore の使い方にて紹介してあるとおり、コレクション→ドキュメントの順に接続していきます。とはset()にてvalueフィールドの値を上書きしています。
これで一通りの実装が完了しましたので、実際にuser_1,user_2でログインしメッセージを書き込んでみてください。それぞれ独立してメッセージが管理されていると思います。

HTML全景

これまで紹介してきたHTMLの全景は次の通りです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Google Firestore Lab</title>
</head>
<body>
  <!-- 認証処理-->
  <div id="auth" style="display: none;">
    <input id="email" type="email">
    <input id="pswd" type="password">
    <button id="loginBtn" type="button">ログイン</button>
  </div>
  <!-- ログイン後-->
  <div id="output" style="display: none;">
    <button id="logoutBtn" type="button">ログアウト</button>
    <p>ユーザーID : <span id="uid"></span></p>
    <input id="message" type="text" placeholder="メッセージを入力">
    <button id="saveBtn" type="button">保存</button>
  </div>
  <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
  <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase-firestore.js"></script>
  <script>
    /*---------------------------------------*/
    /* 1.firebase初期化 */
    /*---------------------------------------*/
    const config = {
        apiKey: "APIキー",
        authDomain: "オースドメイン",
        projectId: "プロジェクトID",
    }
    firebase.initializeApp(config);


    /*---------------------------------------*/
    /* 3.アプリの初期化 */
    /*---------------------------------------*/
    function initApp(){

        /*---------------------------------------*/
        /* 4.DOMセット */
        /*---------------------------------------*/
        const $auth = document.querySelector('#auth');
        const $email = document.querySelector('#email');
        const $pswd = document.querySelector('#pswd');
        const $loginBtn = document.querySelector('#loginBtn');

        const $output = document.querySelector('#output');
        const $logoutBtn = document.querySelector('#logoutBtn');
        const $uid = document.querySelector('#uid');
        const $message = document.querySelector('#message');
        const $saveBtn = document.querySelector('#saveBtn');


        /*---------------------------------------*/
        /* 5.認証状態の監視 */
        /*---------------------------------------*/
        firebase.auth().onAuthStateChanged(user => {

                if(user){

                    /*---------------------------------------*/
                    /* 5.1 ログインしている場合 */
                    /*---------------------------------------*/
                    console.log(user);
                    $uid.innerHTML = user.uid;
                    $auth.style.display = 'none';
                    $output.style.display = 'block';

                    /*---------------------------------------*/
                    /* 8.ユーザーIDと同名のドキュメントに書き込み */
                    /*---------------------------------------*/
                    //- firestoreインスタンス生成
                    const db = firebase.firestore();
                    //- messageコレクションのユーザーIDドキュメントを指定して接続
                    const docRef = db.collection('messages').doc(user.uid);
                    //- イベントリスナー追加
                    $saveBtn.addEventListener('click', event => {
                        docRef.set({
                            value: $message.value
                        }).then(() => {
                            console.log('success');
                        }).catch(error => {
                            console.error(error);
                        });
                    })

                }else{
                    /*---------------------------------------*/
                    /* 5.2 ログインしていない場合 */
                    /*---------------------------------------*/
                    console.log('logged out');
                    $auth.style.display = 'block';
                    $output.style.display = 'none';

                }
        });


        /*---------------------------------------*/
        /* 6.ログインの処理 */
        /*---------------------------------------*/
        $loginBtn.addEventListener('click', event => {
                const email = $email.value;
                const pswd = $pswd.value;
                firebase.auth().signInWithEmailAndPassword(email,pswd).catch(error => {
                    console.error(error);
                });
        });


        /*---------------------------------------*/
        /* 7.ログアウトの処理 */
        /*---------------------------------------*/
        $logoutBtn.addEventListener('click', event => {
              firebase.auth().signOut();
        });

    }
    /*---------------------------------------*/
    /* 2.DOMの読み込みが終わったら */
    /*---------------------------------------*/
    document.addEventListener('DOMContentLoaded',initApp);
  </script>
</body>
</html>

以上のような形です。firebaseを初期化するためのconfigを適宜置き換え、ローカル開発環境にてお試しください。

所感

今回は主にFirebaseの認証処理(Authentication)の説明になりましたが、Firestoreのセキュリティルールもしっかり適用する必要があります。特にユーザーIDによって権限を振り分けるルールは頻繁に使うと思いますので、ご自身でいろいろなルールの記述方法をお試しください。
しかしこんなに簡単にユーザー管理ができるんですね。

NEXT ARTICLE

次はCloud Firestoreを利用したTodoアプリをご紹介できればと考えていますが、0から作ると面倒なのでReactやVueあたりを使うことになるかもしれません。JSフレームワークの説明も入れると長くなっちゃいますので、ポイントを絞った解説になるよう心がけます。といっても、これまでの説明の範疇から出ることはありませんので、ゆっくり習得していきましょう。
へばな!