ユーザー認証情報をFirebase Authenticationへ移設する方法


この記事は Firebase Advent Calendar 2020 3日目の記事です

今回は、既存の別システムにある認証情報を Firebase Authentication へ持ってくる方法を書きます。

タイトルに持ってくるとパンチが強すぎたので、こちらで書きますが実際のところ
平文でパスワード管理してる認証情報をFirebaseへ移設する方法です

実行環境
- [email protected] (Docker container)
- [email protected]

TL;DR

https://github.com/firebase/scrypt コマンドをビルドして、コンバートしたパスワードハッシュを使ってauth:import --hash-alog=SCRYPT

はじめに

基本的には、Firebase の Cli コマンド auth:import を使って import をします。

auth:import と auth:export

ここに、ドキュメントとしては記載があるのですが、Googleさんのドキュメントらしく、
「ふむふむ、で、どうやって?」となること請け合いですので、すこし具体化して参考になるものができればよいな思います。

Firebase Admin SDK のドキュメントの方がもう少し具体化されたトピックを扱ってるので、情報を拾いやすいです。

ユーザーをインポートする

内部ハッシュアルゴリズム

まず、Firebase のパスワード認証ですが、ユーザー作成時のパスワードをハッシュ化して、保存。
認証チャレンジ時に送られてきたパスワード平文を、保存時と同様のアルゴリズムでハッシュ化して、保存されたハッシュと突合して、認証をしています(ざっくり)

内部では、SCRYPT というアルゴリズムを利用してパスワードをハッシュ化しているようです。

注: STANDARD_SCRYPT は標準の scrypt アルゴリズムです。SCRYPT は Firebase Auth の内部で使用している修正バージョンの scrypt です。ある Firebase プロジェクトから別のプロジェクトに移行する場合でも、auth:import で SCRYPT を指定する必要があります。

SCRYPT アルゴリズム自体は、STANDARD_SCRYPT(wikipedia) アルゴリズムをFirebase用に独自に拡張した(ような)もののようで、公開ライブラリとして公開されています。

https://github.com/firebase/scrypt

インポートできる既存システムの認証情報の形式

上記の内部ハッシュ化アルゴリズムを踏まえて、Firebase の Cli では、その他のアルゴリズムでハッシュ化した認証情報を、firebase auth:import 時に指定してimportできます。

ユーザー アカウント ファイル内のパスワードをハッシュするために使用されるアルゴリズム。
パスワード フィールドつきでアカウントをインポートする場合には必須です。 次のいずれかの値になります。BCRYPT、 SCRYPT、STANDARD_SCRYPT、 HMAC_SHA512、HMAC_SHA256、 HMAC_SHA1、HMAC_MD5、MD5、 SHA512、SHA256、SHA1、 PBKDF_SHA1、PBKDF2_SHA256。

  • BCRYPT
  • SCRYPT
  • STANDARD_SCRYPT
  • HMAC_SHA512
  • HMAC_SHA256
  • HMAC_SHA1
  • HMAC_MD5
  • MD5
  • SHA512
  • SHA256
  • SHA1
  • PBKDF_SHA1
  • PBKDF2_SHA256

よって、既存システムの認証情報として Firebase に持ってこれる認証情報は、上記のアルゴリズムで、ハッシュ化して扱っているもののみということになります。

一度別のアルゴリズムで認証情報が作られると、次回該当ユーザーのログイン時には、import時に指定したアルゴリズムで認証を行い、次回以降用にSCRYPTアルゴリズムで再ハッシュ化を行うそうです。

例) MD5ハッシュアルゴリズムを指定してimport
→ 初回ログイン: MD5アルゴリズムを使って照合
→ 内部でSCRYPTアルゴリズムを利用して再ハッシュ化
→ 次回ログイン: SCRYPTアルゴリズムを使って照合

留意点として、import自体は良いのですが、auth:export 時には、SCYPRTアルゴリズム以外でハッシュ化したものは含まれないことが挙げられます。

注: auth:export コマンドは、Firebase のバックエンドで使用される scrypt アルゴリズムを使用してハッシュされたパスワードだけをエクスポートします。

今回はこの中で、特に SCRYPTアルゴリズムを利用した、認証情報の移設について書きます。

新しいシステムでも元のパスワードでログインできるようにしてください

さて、ここで、既存システムを Firebase で置き換えようとしているあなたの元へ、担当者から、

『元のユーザー情報(パスワード)でログインできるようにしてね😁』

と軽くリクエストが来ました。

Firebase以外のシステムでは、軽く殺意を覚えそうなリクエストも、Firebaseなら優しく受け取ってあげられます。

認証情報のエクスポート

まずは、なんらかの方法で既存システムから認証情報をエクスポートします。
エクスポートする情報としては、最低限、

  • メールアドレス: ユーザー識別子
  • パスワード: ハッシュ化されたパスワード

を含むようにします。Json形式などで出しておくと後の取り回しが効きやすいかと。

{
  "users": [
    {
      "email": "[email protected]",
      "passwordHash": "CphRgdZ4WGYUYguqoiaunxDNZqzyeCAIYt0jCjQc5ejFGBOYYR80EUYD1G+bM3uvP8DXCqBbl9aEP5LcjW0PAg==",
      "displayName": "ideodora"
    },

    ...

    {
      "email": "[email protected]",
      "passwordHash": "LJhxyDZ4WGYUYguqoiaunxDNZqzyeCAIYt0jCjQc5ejFGBOYYR80EUYD1G+bM3uvP8DXCqBbl9aEP5LcjW0PAg==",
      "displayName": "ideodoraN"
    }
  ]
}

ハッシュ化アルゴリズムの特定

これは、既存システムの仕様によりけりなので、既存システムの仕様あるいは、内部実装を調べて特定してください。

今回のサンプルでは、

ハッシュ化アルゴリズムは使われておらず、パスワードは暗号化されておりました

まじか。。。((((;゚Д゚)))))))

昨今のシステムでは、よほどパスワードには暗号化は使われていないと思いますが、
古いシステムだと、普通にこんなケースもあると思います。(なんならパスワード平文も…)

「DXがんばりましょう」と、日本のシステム業界に思いを馳せながら、思考を切り替えます。

平文パスワードをSCRYPTアルゴリズムでハッシュ化する

前述の通り、auth:import でインポートできる認証情報には、特定のアルゴリズムでハッシュ化してある必要があります。

かつ、SCRYPT以外のアルゴリズムは、初回ログイン時に別途再ハッシュ化が動いてしまうので、
ここでは、平文のパスワードをSCRYPTアルゴリズムで事前にハッシュ化することを考えます。

今回のサンプルでは、内部実装を調べて、暗号化されたパスワードを復号化しました。。。

{
  "users": [
    {
      "email": "[email protected]",
      "passwordRaw": "123456",
      "displayName": "ideodora"
    },

    ...

    {
      "email": "[email protected]",
      "passwordRaw": "000000",
      "displayName": "ideodoraN"
    }
  ]
}

https://github.com/firebase/scrypt のビルド

https://github.com/firebase/scryptで公開されているソースをビルドして、scrypt コマンドが使えるようにします。

Buid方法はこちら
macOS環境下ではなんら別途注意点が必要なようです。今回はDebianベースの環境でビルドしましたので、
特に嵌りポイントはなくビルドできました。

git clone [email protected]:firebase/scrypt.git
cd scrypt
autoreconf -i
./configure
make

これで、scrypt コマンドが使えるようになります。

一応テスト

# debian で /bin/sh が dash にリダイレクトしてたので。。。
dpkg-reconfigure dash
=> no を選択

make test
=>
make  all-am
make[1]: Entering directory '/src/scrypt'
make[1]: Nothing to be done for 'all-am'.
make[1]: Leaving directory '/src/scrypt'
./tests/test_scrypt.sh .
Using scriptdir /src/scrypt/tests.
Using bindir ..
Using bindir /src/scrypt.
Running tests
-------------
  01-known-value... 
Password: hunter2
Output: 70k8Vg5B3/OrvJOiqusnasv0dLcBHoDAqJrJr7TRLBfMw4MitWx51YXJYFdGiyMbMeKtWtLf5HiBDcN0SUOm4A==
Expected: 70k8Vg5B3/OrvJOiqusnasv0dLcBHoDAqJrJr7TRLBfMw4MitWx51YXJYFdGiyMbMeKtWtLf5HiBDcN0SUOm4A==

SUCCESS!

...

使い方は、リポジトリにある通り
https://github.com/firebase/scrypt#password-hashing

scrypt {key} {salt} {rounds} {memcost} [-P]

A simple password-based encryption utility is available as a demonstration of the scrypt library. It can be invoked as scrypt {key} {salt} {rounds} {memcost} [-P]. The utility will ask for a plain text password and output a hash upon success. This hash should be encoded to base64 and compared to the password hash of the exported user account.

  • {key} - The signer key from the project's password hash parameters. This key must be decoded from base64 before being passed to the utility.
  • {salt} - Concatenation of the password salt from the exported account and the salt separator from the project's password hash parameters. Each half must be decoded from base64 before concatenation.
  • {rounds} - The rounds parameter from the project's password hash parameters.
  • {memcost} - The mem_cost parameter from the project's password hash parameters.
  • [-P] - An optional -P may also be supplied to allow for the raw text password to be read from STDIN.

… なんやけど、すぐ下のサンプルを見ると、{salt} と {rounds} の間に、{salt_separator} の値も必要なので注意

scrypt {key} {salt} {salt_separator} {rounds} {memcost} [-P]

必要なパラメータ

で、それぞれ何を与えればよいのかってとこですが、
そこは、流石の公式なので、Consoleに用意されてます。

Firebase Console > Authentication
で、メニュー「⋮」クリックで「パスワード ハッシュ パラメータ」を表示します。

上記の hash_config の値を拾って、scrypt コマンドにハッシュ化したいパスワードをリダイレクトします。

{key} <= base64_singer_key の値
{salt_separator} <= base64_salt_separator の値
{rounds} <= rounds の値
{memcost} <= mem_cost の値

-P <<< {ハッシュ化したいパスワード}

./scrypt zmIYIgTBHxxxxxxx/xxxxxxxxxxxxxxxxx2ba0Q== c29tZV9zYWx0X3dvcmRz Bw== 8 14 -P <<< 123456
=> Zl6oxxxxxxxxxx/UBAlGv4Qn+yyyyyyyyyyyyyy/tuzIuIQegeF7A==

{salt} には、任意の文字列を base64 した 値を与えてください。

# node
Buffer.from('some_salt_words').toString('base64')
=> c29tZV9zYWx0X3dvcmRz

SCRYPTアルゴリズムでハッシュ化

上記コマンドを用いて、平文のパスワードを改めてハッシュ化します。

{
  "users": [
    {
      "email": "[email protected]",
      "passwordHash": "YCqaoKMP6lr23GK1Zxxxxxx+Wqu9BTDqirHLRPoOowpMgImKamSug5qtAyyyyy/45iU681RGEP2ou0Rh85BtQ==",
      # ↑ 123456 を SCRYPT でハッシュ化したもの
      "displayName": "ideodora"
    },

    ...

    {
      "email": "[email protected]",
      "passwordHash": "8eXK06NjQIN3Hgvrk9xxxxxx+u48Fm2MbOomeDqHepq+wRU6LOyMno1Ejomoryyyyyy6JdD123QYeKwrjqMvgmg==",
      # ↑ 000000 を SCRYPT でハッシュ化したもの
      "displayName": "ideodoraN"
    }
  ]
}

auth:import

前置きがだいぶ長くなりましたが、上記平文のパスワードをSCRYPTでハッシュ化したものを
auth:importでimportします。

生成したパスワードハッシュを用いて、import用のjsonを用意します。
パスワード周り以外のフィールドについては、別途ドキュメントを参照してください。
https://firebase.google.com/docs/cli/auth?hl=ja#JSON

# users.json

{
  "users": [
    {
      "localId": "xxxxx",
      "email": "[email protected]",
      "emailVerified": true,
      "passwordHash": "YCqaoKMP6lr23GK1Zxxxxxx+Wqu9BTDqirHLRPoOowpMgImKamSug5qtAyyyyy/45iU681RGEP2ou0Rh85BtQ==",
      "salt": "c29tZV9zYWx0X3dvcmRz",
      "displayName": "ideodora"
    },
    ...
    {
      "localId": "xxxxn",
      "email": "[email protected]",
      "emailVerified": true,
      "passwordHash": "8eXK06NjQIN3Hgvrk9xxxxxx+u48Fm2MbOomeDqHepq+wRU6LOyMno1Ejomoryyyyyy6JdD123QYeKwrjqMvgmg==",
      "salt": "c29tZV9zYWx0X3dvcmRz",
      "displayName": "ideodoraN"
    },
  ]
}

jsonファイルが用意できたら、firebase-toolsauth:import を利用して アカウント情報を import します。

事前に、
firebase login --no-localhost
firebase use {project_id}

を通して、必要なプロジェクトのfirebase認証を通しておいてください。

firebase auth:import users.json \
--hash-algo=SCRYPT \
--hash-key=zmIYIgTBHxxxxxxx/xxxxxxxxxxxxxxxxx2ba0Q== \
--salt-separator=Bw== \
--rounds=8 \
--mem-cost=14

=>Processing users.json (614 bytes)
Starting importing 2 account(s).
✔  Imported successfully.

--hash-alogにはSCRYPTを指定します。
その他の値については、さきほどコンソールから表示したconfigの値を使います。

auth:import コマンドが通れば、晴れて、平文パスワードをハッシュ化した、
ユーザー認証情報が、firebaseのAuthenticationへimportされます。

ちゃんとハッシュ化された認証情報は…

平文で保存されていない、ちゃんとした認証情報は、上記 auth:import--hash-algo
該当するアルゴリズムを指定して、importできるので、そちらを使いましょう。

たとえば BCRYPT アルゴリズムでハッシュ化している場合

# users_bcrypt.json

{
  "users": [
    {
      "localId": "xxxxxbcrypt",
      "email": "[email protected]",
      "emailVerified": true,
      "passwordHash": "JDJiJDEwJHFBOVFTZ0VWbU5GQXhhTGJ4NXBoWXVGUUdYaThzNFVKbWRaVmxFNzI5Y09mV3U3NnVsSkRD",
      "salt": "JDJiJDEwJHFBOVFTZ0VWbU5GQXhhTGJ4NXBoWXU=",
      "displayName": "ideodoraBCRYPT"
    }
  ]
}
firebase auth:import users_bcrypt.json --hash-algo=BCRYPT

=>
Processing toImports2.json (340 bytes)
Starting importing 1 account(s).
✔  Imported successfully.

まとめ

Firebase 素晴らしい

...

の一言に尽きると思いますが…

とまれ、アカウント移設用のコマンド等、運用に便利な機能が次々に追加されていっているので、
既存のシステムをFirebaseにもってくる為の敷居は、どんどん下がってきてます。みんな使いましょうFirebase。

Auth 周りのエミュレータ機能も先日追加されたようなので、この辺りの機能の検証にも使える…のかな?
時間が取れたら合わせて検証してみたいです。