【解決済み】FastAPIでFirebase AuthenticationのIDトークンをサードパーティーJWTライブラリで検証する方法 〜試行錯誤の過程を添えて〜

意外と同じ構成の情報が見つからず。

ものすごく苦労したので忘れないうちにメモを残しておきます。めちゃくちゃ大変だった。。

前提

用意したアプリケーションは以下2つ。

いずれもGCPの同一プロジェクト内、GAE上にデプロイしてあります。

  • フロント(Nuxt.js)
  • バックエンド(FastAPI)

フロント用のFirebase Authentication SDKを使用してNuxt.js(フロント)でソーシャルログインを実装する方法についてはこの記事では割愛します。(こちらはググると色々と情報が見つかりますのでそこまで困らないかなと。)

今回やりたかったこと

Firebaseよりトークンを受け取りフロント側(Nuxt)でその状態及びデータを保持。

その後リクエストヘッダーにトークンを付与した上でFastAPI(バックエンド)のAPIを叩き、トークンの完全性・信頼度を検証。問題がなければトークン内からユーザーを一位に識別可能なID(uid)等を取得する処理を実装します。←この記事で触れるのはここまで

トークンの検証を終えたあとは、DBを更新したりクライアントからのリクエストに応じてJSONを返したり…、といった処理を適宜実装する想定です。

ID の確認

Firebase Authentication は主にアプリのユーザーを特定して、Cloud Storage などの他のサービスへのアクセスを制限する目的に使用されます。また、独自のサーバーでユーザーを特定するのに利用することもできます。これによって、Firebase Authentication でログインしたユーザーの権限で、サーバー側ロジックを安全に実行できます。

これを行うために、Firebase Authentication でログインしたクライアント アプリケーションから ID トークンを取得し、サーバーに対するリクエストにこのトークンを含めます。サーバーではこの ID トークンを確認し、ユーザーを特定するクレーム(uid、ログインした ID プロバイダなど)を抽出します。これでサーバーはこの ID 情報を使用して、ユーザーの権限でアクションを実行できるようになります。

Admin Auth API の概要 | Firebase

どうやってIDトークンを検証する?

まずはFirebase公式ガイドのここを見ましょう。

ID トークンを検証する | Firebase

上記ガイドにも記載されている通り、クライアントから送信されたIDトークンをバックエンドで検証するには大きく2つの方法があります。

  • Firebase Admin SDK を使用してIDトークンを検証する
  • サードパーティーJWTライブラリを使用してトークンを検証する

まず最初に検討したのは前者でした。

理由はSDK組み込みのメソッドを使うことで比較的シンプルなコードでトークンを検証できるからです。

昔は対応言語が少なかったらしいですが現時点(2020年11月)で「Node.js、Java、Python、Go、C#」に対応していますので、これらの言語を使用している場合はまずこちらの方法を検討するのが良いかと思います。

前者は断念(SDKの初期化が上手くいかない)

ということで「よし!じゃあSDKを使うぞ〜!」と思い色々試したんですが。。

GAE上のFastAPIアプリでSDKの初期化方法が良く分からず断念。

ちなみにSDKの初期化方法については以下に記載があります。

サーバーに Firebase Admin SDK を追加する

というかFirebaseに限らずGAEで外部からアクセスされたくない、プログラム内で使用したい静的ファイル(JSON、秘密鍵、etc)ってどこに保存すればいいんですかね…?結構探したんですがなかなか情報が見つからず。。

GAEでFirebase Admin SDK追加してサービスアカウントキー読み込むのこうすればできるよ〜というのをご存知の方いましたら、是非こちらにお知らせください。

ということで今回は後者の方法を選択することにしました。

疑問:公式のSDKで検証しなくても大丈夫?

(自分含め)認証に関してあまり詳しくない方はFirebase公式のSDKではなくサードパーティのJWTライブラリでトークンを検証して問題ないの?と思うかもしれませんがこれは全く問題なさそうです。

この辺に関しては「JWTって何?OAuthってどんな仕組み?そもそも認証って何をするの?」といったあたりを学ぶと良いかもしれません。

自分が特に参考・勉強になった記事を以下に貼っておくので、気になった方はぜひチェックしてみてください。

実装

ということで方針が決まりましたので実際に組んでいきます。

実装にあたってはFastAPI公式ドキュメント以下記事「Update the dependencies」部分のコードを参考しました。FastAPIの公式ドキュメントはかなり手厚いのでじっくり読めばかなり参考になります。(但し結構重い。読むのつらい)

OAuth2 with Password (and hashing), Bearer with JWT tokens – FastAPI

なお、フロントから送信されるトークンはリクエストヘッダ(Authorization: Bearer)に付与、JWTの検証には上記ページでも紹介されている「python-jose」を使用しています。

完成コードは以下の通りです。(必要最低限の箇所のみ抜粋)

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from starlette.middleware.cors import CORSMiddleware
import os
import json
import urllib3
from jose import JWTError, jwt


app = FastAPI()

# CORS設定(クライアントのoriginを必要に応じて追記。開発中はlocalhostも必要になるはず)
origins = [
    "https://hogehoge.com",
    "http://localhost:3000",
]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


def get_current_user(cred: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
    # エラー時の処理
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    # JWTトークン取得
    token = cred.credentials
    # FirebaseプロジェクトID(自分のものに置き換えてください)
    target_audience = "MY_FIREBASE_PROJECT_ID"
    # Google公開鍵情報の取得
    google_cert_url = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
    http = urllib3.PoolManager()
    res = http.request('GET', google_cert_url)
    secret_keys = json.loads(res.data.decode('utf-8'))
    try:
        # kidを取り出すために一旦JWTトークンheaderを取得
        token_header = jwt.get_unverified_header(token)
        kid = token_header["kid"]
        secret_key = secret_keys[kid]
        # トークンをデコード(エラーが生じた時点で改竄を検知できる)
        payload = jwt.decode(token, secret_key, algorithms='RS256', audience=target_audience)
    except JWTError:
        raise credentials_exception
    return payload


# ログイン状態でこのエンドポイントを叩く。トークンに問題がなければユーザー情報を返却する
@app.get('/check-user/')
async def check_user(current_user=Depends(get_current_user)):
    return {'msg': 'ok', 'user': current_user}

必要なパッケージは以下の通りです。

requirements.txtを使用している場合は以下を追記してデプロイ前に「pip install -r requirements.txt」でインストールしておきましょう。(各バージョンは検証時使用したもの。ご自身の環境に合わせて変更してください。)

python-jose==3.2.0
cryptography==3.2.1
urllib3==1.26.2

# 以下任意:本記事と関係ないけどFastAPIで大体必要になるもの
fastapi==0.61.2
gunicorn==20.0.4
uvicorn==0.11.2

ハマったことなど

最後に実装時にいくつかハマったこと・詰まったことを紹介します。

ハマり①:トークンの内容に問題がないはずなのにレスポンスが401エラー

一番解決に時間がかかったところです。

先にも挙げたFirebase公式のID トークンを検証するに記載の通りトークンをデコード(検証)する処理を書いて、クライアント(Nuxt)から対象のAPIを叩いてみたんですが、最初何度行ってもレスポンスが401となりトークンの検証が通りませんでした。。

で、詰まりまくってたときに辿り着いたのが以下Stack Overflowのエントリー。

How to decode Firebase JWT token in Python – Stack Overflow

この中のベストアンサー内のソースコードで、python-joseを使用したデコード時(jwt.decode〜部分)に「audience」の指定があることを発見。ソースに記載の通りここにFirebaseのプロジェクトIDを指定したところデコードが正常終了。エラーにならずレスポンスが得られるようになりました!(ペイロード内の他の項目に関しては特に意識することなく条件を満たしているようなので気付かなかった)

JWTや認証に関して疎かったということもあり「ペイロードはあくまでデータ」「トークンのデコード(検証)は署名さえ問題なければ良い」と思い込んでいたためハマってしまいました。。

まとめると。FirebaseのIDトークンのサードパーティJWTライブラリを使用した検証にあたっては、デコード時に以下の要件を満たしている(要件を満たしていることが分かるよう引数を渡す)必要がありました。

内訳項目
ヘッダーアルゴリズム(alg)“RS256”
ヘッダー鍵ID(kid)https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.comに記載されたいずれかの公開鍵に対応する必要がある
ペイロード対象(aud)Firebase プロジェクトの ID

↓つまりこういうこと(MY_FIREBASE_PROJECT_IDには自分のFirebaseプロジェクトIDを入れます)

# NG
payload = jwt.decode(token, secret_key, algorithms='RS256')

# OK!
payload = jwt.decode(token, secret_key, algorithms='RS256', audience='MY_FIREBASE_PROJECT_ID')

ハマり②:トークンのヘッダー内のkidを取り出して対応する公開鍵を確認したいけど…どうやって?

上記でも触れた通り、Firebaseの場合指定されたURLで公開されている公開鍵情報の中からトークンのヘッダー内のkidと一致するキーの公開鍵を取得、デコード時に指定してあげる必要があります。

実装方法として難しいことは特にないんですが、JWTの理解が全くなかったときは「デコード前のトークンからkidを取り出す…?」というのが良く分からなかったので、悩んだ記録としてここに書き残させてください。笑

どういう手順で確認すれば良いのか?ということについては以下記事「トークンの検証」が参考になりました。

Firebase Authentication を使って得られた知見まとめ – トークンの仕様や注意点など – slideship.com

そんなこんなで

なんとか実装できました。。長かった。。

とりあえず忘れないうちに書き殴っておこうということで、粗いですがこんなところで。

今回自分はGAEにデプロイ、バックエンドにFastAPIを使用していますが、FlaskでもDjangoでも他のクラウド・サーバーにデプロイしたアプリでもトークン検証の部分は流用できるはずです。

どなたかの参考になれば幸いです!

参考記事など

以下、上記で記載したもの以外で参考になった記事たち。

公式Doc

User Guide – urllib3 2.0.0.dev0 documentation

ブログ

こちらの記事もとても参考になりました。感謝!

PythonでFirebase Authenticationのトークン取得とFastAPIでトークン検証 – Qiita

RubyでFirebaseのidトークンを認証に使ってみる – Qiita

Firebase Auth のユーザ認証機能を自前のデータベースと連携する – Qiita

Firebase から取得したIDトークンをサーバサイド(Java+SpringBoot)で検証する – Qiita

Firebase Admin SDKで一般的なWebサービスの構成にFirebase Authenticationを使った認証処理を組み込む。 – Qiita

Cloud Functions で構築したREST APIをFirebase認証で保護する。そして自前RESTサーバのAPIにもFirebase認証を適用する。 – Qiita

JSON Web Signatureを簡単かつ安全に使うためのkid/typパラメータの使い方

よかったらシェアしてね!
  • URLをコピーしました!