🔑
【Firebase Authentication】 IdToken検証を自前で実装してみた
前書き
最近仕事でFirebase Authenticationを利用する機会があり、IdTokenの検証がたったの一行のコードで完了するのに驚き感じたと同時に中でどういうアルゴリズムで検証しているのか気になったので少し深掘りしてみました。
トークン検証の関数
verify_id_token(idToken)
前提知識
IdTokenはJWT(JSON Web Token)の形式で構成されています。
header.payload.signature
| 部分 | 内容 |
|---|---|
header |
使用アルゴリズム(例: RS256)など |
payload |
トークンの中身(例: sub, aud, exp など) |
signature |
header + payload を秘密鍵で署名したもの |
検証
バックエンドはFastAPIで構築しており、色々なサイトを見ているとget_current_userのアルゴリズムで検証⇨ペイロードの(ユーザ固有の情報を格納)取得をすることが多いらしいです。
credにフロントエンドから取得した認証情報が入り、cred.credentialsにBearerに含まれるidTokenが格納されています。
以下は実際の認証エンドポイントから一部コードを抜き出したものです。
from fastapi import APIRouter, HTTPException, Depends, status
from pydantic import BaseModel
import firebase_admin
from firebase_admin import credentials, auth
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
cred = credentials.Certificate("your-json-path")
firebase_admin.initialize_app(cred)
print("cred.credentials")
print(cred.get_credential)
router = APIRouter()
def get_current_user(cred: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
if not cred:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"}
)
try:
print("cred.credentials")
print(cred.credentials)
cred = auth.verify_id_token(cred.credentials)
except:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"}
)
return cred
idTokenを正当性を検証するのがget_current_user関数の以下の部分です。
cred = auth.verify_id_token(cred.credentials)
この処理をChatGPTとの対話を通してトライアンドエラーで実装しました。
早速、実装したコードの概要は以下です。
import base64
import json
import requests
import time
import jwt
from jwt.exceptions import InvalidTokenError
from cryptography.hazmat.primitives import serialization
from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.backends import default_backend
FIREBASE_PROJECT_ID = "your-project-id"
# Firebase Authentication がクライアントSDKで発行したIDトークンの署名検証に必要な公開鍵を配布するための Google公式の共通エンドポイント
FIREBASE_CERT_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
# X.509証明書からRSA公開鍵を抽出
def convert_cert_to_rsa_key(cert_str: str):
cert_obj = load_pem_x509_certificate(cert_str.encode('utf-8'), default_backend())
public_key = cert_obj.public_key()
return public_key
# 公開鍵(X.509証明書)を取得(通常はキャッシュされる)
def get_firebase_public_keys():
response = requests.get(FIREBASE_CERT_URL)
response.raise_for_status()
return response.json() # dict: {kid: public_key}
# トークンの検証
def verify_id_token_simulated(token: str):
try:
# ヘッダーを取得してkid(鍵ID)を確認
headers = jwt.get_unverified_header(token)
kid = headers.get("kid")
if not kid:
raise ValueError("Missing 'kid' in token header")
# 公開鍵を取得
public_keys = get_firebase_public_keys()
if kid not in public_keys:
raise ValueError("Unknown 'kid' in token")
# X.509証明書を取得。
cert_str = public_keys[kid]
# X.509証明書をRSA公開鍵に変換。
public_key = convert_cert_to_rsa_key(cert_str) # ← ここでRSA公開鍵を抽出
# デコード(署名検証あり)
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience=FIREBASE_PROJECT_ID,
issuer=f"https://securetoken.google.com/{FIREBASE_PROJECT_ID}"
)
# 明示的に 'sub' の存在も確認
if not decoded.get("sub"):
raise ValueError("Token missing 'sub' claim")
# 成功
return decoded
except InvalidTokenError as e:
raise ValueError(f"Invalid Firebase ID token: {str(e)}")
# 使用例
if __name__ == "__main__":
token = "thisis.your.token"
try:
decoded = verify_id_token_simulated(token)
print("✅ トークンOK:", decoded)
except Exception as e:
print("❌ トークンNG:", e)
処理の流れをざっくり示すと、以下です。
- idTokenのheaderから鍵ID(kidキーの値)を取得。
- 公開鍵を取得。
- 公開鍵から1で取得した鍵IDをキーにする値(X.509証明書)を取得。
- 4で取得したX.509証明書からRSA公開鍵を抽出。
- RSA公開鍵とtokenを元に署名の検証+ペイロード部分を複合する。
署名の検証とは、具体的にidTokenの署名部分に対してRSA公開鍵で検証(=対応する秘密鍵で署名されたものかを検証)します。
実際にfirebaseから取得したidTokenをtokenに格納してプログラムを実行すると、正常に完了した場合はペイロード部分がデコードされた値が返ってくるはずです。
もし有効期限が切れてたり、idTokenを改竄すると認証が失敗します。
Discussion