Firebase Authentication - その3

「Firebase のユーザー認証システムで、Web クライアント認証を実装してみます」 の続きです.
今回は、サーバ側で認証済のクライアントかどうかを確かめるために JWT の検証をしてみるです.


クライアントで JWT を取得する

前回までにクライアント側でログイン認証を行うところまで完了しました。 次にサーバ側で、正しくログインされたクライアントからのアクセスか判断する方法を見てみます。

サーバ側で、許可されたデバイス (クライアント) からのアクセスか判定するために JWT(JSON Web Token) と呼ばれる仕組みが使われます。

JWT の詳しい説明は省きますが、簡単にいうと、デバイスの属性情報の JSON に署名をしたものになります。 サーバ側では署名を検証することで、正規のデバイス (今回は、正しくログインされた WEB クライアント) かどうかを判断して処理をするか決めます。

Firebase では、JWT を得るために、 getIdToken() を使います。 前回、 Home.vue のところで後ほどと書いた部分です。

  mounted: function () {
    firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        // ログインしていれば中通る
        this.user = user;
        user.getIdToken(true).then((token) => {
          this.token = token;
          let tokens = token.split('.');
          console.log(base64url.decode(tokens[0]));
          console.log(base64url.decode(tokens[1]));
        });
      } else {
        this.token = null;
      }
    });
  },

上のコードでは、中身を確認するために、ログ出力しています。 JWT は、 base64url (URL セーフな base64 エンコード) された JSON データです。 ヘッダーペイロード署名 の3つ部分を ‘.’ で連結したものです。

Authorization ヘッダー

JWT をサーバーに送る必要がありますが、Web クライアントから API を叩くことを仮定して axios でサーバをアクセスするときに、トークンを追加するようにしてみます。

onVerifyID メソッドを <a> アンカーから呼びます。呼び出す処理は以下になります。

  methods: {
    onVerifyID: function () {
      console.log("onVerifyID");
      const config = {
        headers: { Authorization: `Bearer ${this.token}` },
      };
      this.verifyResult = null;
      const api_client = axios.create();
      api_client
        .get(API_URL, config)
        .then((result) => {
          console.log(result);
          this.verifyResult = result.data;
          })
        .catch((err) => {
          console.log(err);
          this.verifyResult = 'エラー';
        });
    },
  },

axios で HTTP リクエストする際のヘッダーに、 Authorization: Bearer + JWTトークン を追加します。 今回はリクエストは 1箇所だけなので、直接ヘッダーを指定していますが、通常なら defaults config などで設定すると思います。

API_URL には、アクセステストをするための URL を指定しますが、この受け口は、次に作成します。

ここまでの変更は、以下のブランチで確認できます。

vue-firebase-auth - request_headers ブランチ

テストサーバ

サーバ側ですが、認証情報のテストをする必要最低限の実装をします。

Node.js の express でサーバを準備します。サーバ側も詳しく説明していくと長くなるので、 ここでは簡単にキモになるところだけ触れます。

const express = require('express');
const cors = require('cors');
const admin = require('firebase-admin');

const serviceAccount = require('./serviceAccount.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

admin がFirebase のサーバ側の管理 API です。 admin.initializeApp() で API に接続します。 Firebase Admin API にアクセするためには、サービスアカウントを通したアクセスが必要になります。 Google のクラウド上で実行している場合には自動的に設定されますが、ここではローカルでテストしているので、 ローカルファイルに配置した認証情報を読み込んで、 credential に設定しています。

const app = express();
app.use(cors());

app.get('/', verifyToken, (req, res) => {
  // console.log(req);
  res.status(200).send('IDを確認しました.').end();
});

function verifyToken(req, res, next) {
  const bearerHeader = req.headers['authorization'];
  if (typeof bearerHeader !== 'undefined') {
    const bearer = bearerHeader.split(' ');
    const token = bearer[1];
    // console.log('token:', token);
    admin
      .auth()
      .verifyIdToken(token)
      // eslint-disable-next-line no-unused-vars
      .then((_decoded) => {
        // console.log('verified:', decoded);
        // req.local = decoded.name;
        next();
      })
      .catch(error => {
        console.log(error);
        res.sendStatus(500);
      });
  } else {
    res.sendStatus(403);
  }
}

get('/') にミドルウェア verifyToken を噛ませます。

この中が、クライアントから送られた JWT の検証をしている部分になります。 Authorization: Bearer ヘッダーに追加されてきた JWT (クライアント側で getIdToken() で取得したもの) を verifyIdToken() に渡すと、あとは勝手に署名を検証してくれます。

// Start the server
const PORT = process.env.PORT || 8030;
app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`);
  console.log('Press Ctrl+C to quit.');
});
// [END gae_node_request_example]

module.exports = app;

残りは、サーバ を準備するだけです。 あと、この例では、cors 全許可になりますので、公開サーバではその辺はきちんと設定を。

サーバ側のコードは、以下に置いておきます。

Github - firebase-test-server

最後に

Firebase の Authentication を使って、 Web クライアントにユーザー認証の機能を組み込んでみました。 実際のコードを見ると分かるかと思いますが、実装の方は API 側に任せっきりで、コードとしては、 ほぼ 1行呼び出しで済んでしまう良い時代になったという感じです。