Firebase 入門(3) ~ セキュリティの向上

Authentication

Database ルール修正

今のままだと、データベースがどのユーザーでもアクセスできてしまうので、アクセス制限をする。まず、Database のルールを以下のように変更。内容としては、認証されたuidと同じ場合のみアクセスを許可している。

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid"
      }
    }
  }
}

Firebase Authentication

Firebase には認証を管理する Authentication というサービスがある。それを使ってユーザーを認証し、uidを取得し、データベースにアクセスできるようにする。今回は Authentication を有効にして、「ログイン方法」の「メール/パスワード」のみを有効にした。

firebaseui

Firebase はデフォルトでログインフォームUIを提供している。それが firebaseui だ。まずは npmモジュールをインストールする。

$ npm i firebaseui --save

firebaseui 自体の使い方は簡単で、FirebaseUI インスタンスを Auth インスタンスで初期化して、インスタンスを設定オブジェクトでスタートしてやるだけでいい。

import firebase from 'firebase/app';
import 'firebase/database';
import 'firebase/auth';
import firebaseui from 'firebaseui'

const config = {
  apiKey: "<API_KEY>",
  authDomain: "<PROJECT_ID>.firebaseapp.com",
  databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
  projectId: "<PROJECT_ID>",
  storageBucket: "<BUCKET>.appspot.com",
  messagingSenderId: "<SENDER_ID>",
};

firebase.initializeApp(config);

var uiConfig = {
  callbacks: {
    signInSuccessWithAuthResult: function(authResult, redirectUrl) {
      return true;
    },
    uiShown: function() {
      document.getElementById('loading').style.display = 'none';
    }
  },
  signInFlow: 'popup',
  signInSuccessUrl: '/',
  signInOptions: [
    firebase.auth.EmailAuthProvider.PROVIDER_ID
  ],
  tosUrl: '/term_of_service',
  privacyPolicyUrl: '/privacy_policy'
};

var ui = new firebaseui.auth.AuthUI(firebase.auth());

ui.start('#firebaseui-auth-container', uiConfig);

ui.start() の第一引数で指定したIDの要素にログインフォームが表示される。

React で使う

React で使うなら、以下のような感じだろうか? LoginUI 関数の中が本体。多重読み込みを防ぐために、useEffect の 第二引数にダミーのstateを突っ込んでいるのがちょっとダサい。あと、この後データベースとの連携もあるので useReducer を使ってアクションを作っているが、なくても動く。

import * as React from 'react';
const { useState, useEffect, createContext, useReducer, useContext } = React;
import firebase from "firebase/app";
import 'firebase/database';
import 'firebase/auth';
import firebaseui from 'firebaseui';

const DispatchContext = createContext(() => {});

const reducer = (state, action) => {
  switch(action.type) {
    case 'change_user':
      if(state.uid === action.payload.uid) {
        return state
      }
      return { username: action.payload.username, uid: action.payload.uid }
    case 'change_username':
      if(state.username === action.payload.username) {
        return state
      }
      return { username: action.payload.username, uid: state.uid }
  }
};

const LoginUI = () => {
  const dispatch = useContext(DispatchContext);

  const [temp, _] = useState(0);

  useEffect(() => {
    const deffer = () => {}

    firebase.auth().onAuthStateChanged(function(user) {
      if(firebase.auth().currentUser !== null) {
        dispatch({type: 'change_user', payload: { username: user.displayName, uid: user.uid }})
        return deffer
      }

      var uiConfig = {
        callbacks: {
          signInSuccessWithAuthResult: function(authResult, redirectUrl) {
            return true;
          },
          uiShown: function() {
            // document.getElementById('loading').style.display = 'none';
          }
        },
        signInFlow: 'popup',
        signInSuccessUrl: '/',
        signInOptions: [
          firebase.auth.EmailAuthProvider.PROVIDER_ID
        ],
        tosUrl: '/term_of_service',
        privacyPolicyUrl: '/privacy_policy'
      };

      var ui = new firebaseui.auth.AuthUI(firebase.auth());

      ui.start('#firebaseui-auth-container', uiConfig);

      return deffer
    });
  }, [temp]);

  return (
    <>
      <div id="firebaseui-auth-container"></div>
    </>
  );
}

export default () => {
  const [state, dispatch] = useReducer(reducer, {
    username: "",
    uid: "",
  });

  const config = {
    apiKey: "<API_KEY>",
    authDomain: "<PROJECT_ID>.firebaseapp.com",
    databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
    projectId: "<PROJECT_ID>",
    storageBucket: "<BUCKET>.appspot.com",
    messagingSenderId: "<SENDER_ID>",
  };

  if (!firebase.apps.length) {
    firebase.initializeApp(config);
  }

  return (
    <>
      <div>{state.username}</div>
      <DispatchContext.Provider value={dispatch}>
        <LoginUI />
      </DispatchContext.Provider>
    </>
  );
};

Database と連携

App コンポーネントレンダリングされたタイミングでデータベースにアクセスしてユーザー情報を取得するようなものを作成した。先ほどの例の default 関数に useEffect Hook で実装する。users/<uid> のパスにアクセスして何も取得できない場合は、stateの情報をもとに箱を作成する。state が更新されるたびにデータベースにアクセスしにいかないように、state.uidが更新されたタイミングでアクセスするようにもした。

  useEffect(() => {
    if(state.uid =="") {
      return () => {}
    }

    database.ref('users/' + state.uid).once('value').then((snapshot) => {
      if(snapshot.val() === null) {
        database.ref('users/' + state.uid).set({
          username: state.username
        });
        dispatch({type: 'change_username', payload: { username: state.username}})
      } else {
        const newUsername = snapshot.val().username;
        dispatch({type: 'change_username', payload: { username: newUsername}})
      }
    })

  }, [state.uid])

おまけ (Signout ボタン)

デバッグで必要だったのでSignoutボタンをつけた。

export default 関数に Logout コンポーネントを追加して、Logoutコンポーネント側でボタンを作って onClickイベント関数の中で firebase.auth().signOut() メソッドを読んでいるだけ。

const LogoutUI = () => {
  const signOut = () => {
    firebase.auth().signOut().then(function() {
      console.log("signed out")
    }).catch(function(error) {
      console.log(error)
    });
  }
  return (
    <button onClick={signOut}>Sign out</button>
  )
}

export default () => {
  // 省略
  return (
    <>
    <div>{state.username}</div>
    <DispatchContext.Provider value={dispatch}>
      <LoginUI />
      <LogoutUI />
    </DispatchContext.Provider>
    </>
  );
}