VIVITABLOG

VIVITAで活躍するメンバーの情報発信サイト

Firebaseを使ったシングルサインオン

VIVITAソフトウェアエンジニアの板本@itamotoです。

Firebaseを利用して、サーバー無しでお手軽に動くものをサクッと作ることは良くあるケースだと思います。
プロトタイプを作って、そのまま使い続けるものがいくつか出てきたので、それらを認証を共有して連携させる試みです。 WEBサーバーを別途追加した構成でシングルサインオンを試してみました。

処理の流れ

f:id:itamoto:20200112215559p:plain

Firebase側の設定

Authentication

今回はGoogleアカウントを使った認証を使います。 f:id:itamoto:20200110011903p:plain

Cloud Firestore

データベースのルールには以下を設定
認証したユーザだけが読み書きできるデータを作ります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if (request.auth.uid == userId);
    }
  }
}

以下のようにデータを作成しました。
ドキュメント部分にはuidを設定していますが、後述するフロントエンドで実際にログインをしてfirebase上に登録されたユーザー情報を確認しています。
(本来であれば書き込み機能があれば良いので、事前にデータ作成は必要ないです。) f:id:itamoto:20200110012410p:plain

フロントエンド

access tokenの取得

アプリ1でaccess tokenを取得する処理

import firebase from 'firebase';

const provider = new firebase.auth.GoogleAuthProvider();
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
this.$store.getters.firebase
  .auth()
  .signInWithPopup(provider)
  .then(result => {
    console.log(result.credential.accessToken);  // access token
  })
  .catch(function(error) {
    console.error(errorCode + ' : ' + errorMessage);
  });

アプリ2からaccess token付きでAPIをリクエスト

import axios from 'axios';
const response = await axios.get(
  'http://[serverHost]/users/me/profile',
  {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`
    }
  }
);
console.log(response);

サーバーサイド

firestoreからユーザーの情報を取得して返すAPI

access tokenを受け取ってuidを抽出

const fbAdmin = require('firebase-admin');
const restify = require('restify');
let fbDefault;
let fbUser;
~~~~~~
const firebaseInitializeApp = async uid => {
  if (!uid) {
    if (!fbDefault) {
      fbDefault = fbAdmin.initializeApp(
        {
          credential: fbAdmin.credential.applicationDefault(),
          databaseURL: 'https://xxxxxx.firebaseio.com',
          databaseAuthVariableOverride: {
            uid: uid
          }
        },
        'userRole'
      );
    }
  } else {
    if (!fbUser) {
      fbUser = fbAdmin.initializeApp({
        credential: fbAdmin.credential.applicationDefault(),
        databaseURL: 'https://xxxxxx.firebaseio.com'
      });
    }
  }
};

server.get('/users/me/profile', async (req, res, next) => {
  allowCors(res);
  try {
    const uid = await tokenCheck(req.header('Authorization'));
    await firebaseInitializeApp(uid);

    const userSnapshot = await fbUser
      .firestore()
      .collection('users')
      .doc(uid)
      .get();

    res.send(200, {
      result: 'ok',
      id: uid,
      nickname: userSnapshot.get('nickname')
    });

    return next();
  } catch (error) {
    return next(
      new InternalError({
        toJSON: () => ({
          errors: [{ message: 'InternalError' }]
        })
      })
    );
  }
});

const tokenCheck = async token => {
  firebaseInitializeApp();
  const decodedToken = await fbDefault.auth().verifyIdToken(token);
  const firestore = fbDefault.firestore();

  return decodedToken.uid;
};

まとめ

Firebaseを使ったアプリをどんどん連携していきたいところですが、 シングルサインオンで次々つなげて行く場合、Authenticationの元になるFirestoreに複数のアプリのデータを集約しなければならないのが懸念です。
今の所Cloud Firesoreはプロジェクトで1つしか作れないですしね。
今後試してみてより良い方法があれば、またまとめてみようと思います。