Rails + Firebase Authentication EmulatorでverifyIdTokenしたい
経緯
RailsをAPIにして 「RailsからFirebase AuthenticationをREST API経由で呼び出して
ユーザー情報のやりとりをする必要がー」という方は(きっと)いると思います。
私はよくやっています。
どういうこと?という方は以下の記事を参考にするといいと思います
とてもわかりやすくまとまっています。
このケースでの話です。
残念なことにRubyにはfirebase Admin SDKがないので以下を自前でやっています。
(何かいい方法があれば教えて下さい)
- フロントエンドから送られてきたidTokenを検証(改ざん等されていないかチェック)
- 検証結果が正しければuidを使ってアプリケーション内でのユーザー認証
今更ですが、Firebaseにエミュレーターがあるって知り
今の開発環境に組み込んでみたときに
詰まったところがあるのでその経緯等を残しておこうと思った次第です。
adminSDKがある言語は回避方法が別にあるので、それ以外の言語の方向けです。
では、前置きが長くなりましたが
本題です。
問題
idTokenを検証する際にエミュレーターと本物のfirebaseが発行するidTokenに違いがある
これが今回の本題です。
JWTデコードしたあと、Firebaseのドキュメントにあるように各項目が改ざんされていないかチェックしていくわけですが
Firebaseエミュレーターが作るIDトークンヘッダーにはalgとkidが未設定です
これはバグというわけではなくて、
開発者側がセキュリティを考慮して意図してやっているようです。
(このissueで議論されています。)
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のを調べてみました。
エミュレータの場合はcheckSignatureがスキップされています
これはこの部分のチェックですね
最後に、トークンの kid 要件に対応する秘密鍵によって ID トークンが署名されたことを確認します。https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com から公開鍵を取得し、JWT ライブラリを使用して署名を確認します。
@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の単体項目チェックのとこにもエミューレーターかどうかの条件が入ってます
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