🔥

Rails + Firebase Authentication EmulatorでverifyIdTokenしたい

2022/02/15に公開

経緯

RailsをAPIにして 「RailsからFirebase AuthenticationをREST API経由で呼び出して
ユーザー情報のやりとりをする必要がー」という方は(きっと)いると思います。
私はよくやっています。

どういうこと?という方は以下の記事を参考にするといいと思います
とてもわかりやすくまとまっています。
このケースでの話です。
https://zenn.dev/awakei/articles/775c030922bf9c6ab56e

残念なことにRubyにはfirebase Admin SDKがないので以下を自前でやっています。
(何かいい方法があれば教えて下さい)

  • フロントエンドから送られてきたidTokenを検証(改ざん等されていないかチェック)
  • 検証結果が正しければuidを使ってアプリケーション内でのユーザー認証

今更ですが、Firebaseにエミュレーターがあるって知り
今の開発環境に組み込んでみたときに
詰まったところがあるのでその経緯等を残しておこうと思った次第です。
adminSDKがある言語は回避方法が別にあるので、それ以外の言語の方向けです。

では、前置きが長くなりましたが
本題です。

問題

idTokenを検証する際にエミュレーターと本物のfirebaseが発行するidTokenに違いがある

これが今回の本題です。
JWTデコードしたあと、Firebaseのドキュメントにあるように各項目が改ざんされていないかチェックしていくわけですが
Firebaseエミュレーターが作るIDトークンヘッダーにはalgとkidが未設定です

これはバグというわけではなくて、
開発者側がセキュリティを考慮して意図してやっているようです。
(このissueで議論されています。)
https://github.com/firebase/firebase-tools/issues/2764

algとkidの各検証およびkidがないので以下の検証ができません

最後に、トークンの kid 要件に対応する秘密鍵によって ID トークンが署名されたことを確認します。https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com から公開鍵を取得し、JWT ライブラリを使用して署名を確認します。

ちなみにFirebase Admin SDKを使ってidTokenを検証(verifyIdToken)できる環境の場合は
この対応が既にされていて、
私の方では未検証ですがエミュレーターでも特に問題なく使えているはずです。
(環境変数にFIREBASE_AUTH_EMULATOR_HOSTが設定されている場合、エミュレーター対応が動作します)

結論(対応)

エミュレーター使ってる時はkidとalgのチェックをスキップ!!

身も蓋もない結論ですいません。

経緯はFirebase Admin SDKではどう対応しているのか、その対応方法に合わせようと思い、
Javaのを調べてみました。

https://github.com/firebase/firebase-admin-java/blob/c213726b6bb63c208c102583c1ddeea773bda15f/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java#L96-L107

エミュレータの場合はcheckSignatureがスキップされています
これはこの部分のチェックですね

最後に、トークンの kid 要件に対応する秘密鍵によって ID トークンが署名されたことを確認します。https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com から公開鍵を取得し、JWT ライブラリを使用して署名を確認します。

FirebaseTokenVerifierImpl.java
  @Override
  public FirebaseToken verifyToken(String token) throws FirebaseAuthException {
    boolean isEmulatorMode = Utils.isEmulatorMode();
    IdToken idToken = parse(token);
    checkContents(idToken, isEmulatorMode);
    if (!isEmulatorMode) {
      checkSignature(idToken);
    }
    FirebaseToken firebaseToken = new FirebaseToken(idToken.getPayload());
    checkTenantId(firebaseToken);
    return firebaseToken;
  }

kidとalgの単体項目チェックのとこにもエミューレーターかどうかの条件が入ってます

FirebaseTokenVerifierImpl.java
    if (!isEmulatorMode && header.getKeyId() == null) {
      errorMessage = getErrorForTokenWithoutKid(header, payload);
    } else if (!isEmulatorMode && !RS256.equals(header.getAlgorithm())) {
      errorMessage = String.format(
          "Firebase %s has incorrect algorithm. Expected \"%s\" but got \"%s\".",
          shortName,
          RS256,
          header.getAlgorithm());
    }

エミュレーターがどうか判定している部分はこんな感じ
問題のセクションで少し触れましたが
環境変数にFIREBASE_AUTH_EMULATOR_HOSTが設定されているかどうかが条件になります。

public class Utils {
  @VisibleForTesting
  public static final String AUTH_EMULATOR_HOST = "FIREBASE_AUTH_EMULATOR_HOST";

  public static boolean isEmulatorMode() {
    return !Strings.isNullOrEmpty(getEmulatorHost());
  }

  public static String getEmulatorHost() {
    return FirebaseProcessEnvironment.getenv(AUTH_EMULATOR_HOST);
  }

}

どういう対応を取るべきか悩んで、何かエビデンスが欲しくて
色々調べたので、折角なので記事にしました。

エミュレーターできちんと環境構築したいのに
認証が通らない!という方への少しでも助けになれば幸いです。

Discussion