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

# やること

FirebaseとFastAPIを用いて、クライアント側でトークンを取得し、自前のAPIを叩くときにヘッダーに貼り付け、サーバーサイドで検証することでログインしているかどうかを判断します。ここではGoogle謹製の`firebase_admin`を用いてトークンを検証します。


# トークンを取得する

FirebaseコンソールのAuthenticationでパスワードログインを有効にし、以下のような適当なアカウントを作ります。ここではこれを用いてログインします。もちろんTwitterなどの認可でも大丈夫です。

```python
EMAIL = 'test001@example.com'
PASSWORD = 'password'
```

必要なものをインストールします。

```
$ pip install requests
```

firebaseコンソールから以下のようなJSONを取得し貼り付け良い感じに成形します。(実際に使うのは`apiKey`のみ)

```python
CONFIG = {
    "apiKey": "YOUR API KEY",
    "authDomain": "YOURPROJECTID.firebaseapp.com",
    "databaseURL": "https://YOURPROJECTID.firebaseio.com",
    "projectId": "YOUR PROJECT ID",
    "storageBucket": "YOUR PROJECT ID.appspot.com",
    "messagingSenderId": "YOUR MESSAGE SENDER ID",
    "appId": "YOUR:APP:ID",
    "measurementId": "YOUR MEASUREMENT ID"
}
```

Firebase AuthのREST APIを叩き、tokenを取得します。[REST APIのドキュメントはここです。](https://firebase.google.com/docs/reference/rest/auth/#section-sign-in-email-password)

```python
api_key = CONFIG["apiKey"]
uri = f"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={api_key}"
data = {"email": EMAIL, "password": PASSWORD, "returnSecureToken": True}

result = requests.post(url=uri, json=data).json()

token = result['idToken']
```

このtokenを実行時に出力し、後で用います。

# FastAPIでトークンを検証する

必要なものをインストールします。

```
$ pip install fastapi firebase_admin uvicorn
```

firebaseのコンソールから歯車→サービスアカウント→新しい秘密鍵の生成で秘密鍵をダウンロードし、読み込みます。

```python
from firebase_admin import auth, credentials

cred = credentials.Certificate("path/to/cert.json")
firebase_admin.initialize_app(cred)
```

ヘッダーからトークンを取得し、デコードし、ユーザー情報を取得する関数を定義します。
実際はここでDBからユーザー情報を取得しても良いと思います。
FastAPIのドキュメントには`fastapi.security.OAuth2PasswordBearer`の使用例は書かれていたのですが、単純にBearerトークンを取得したい時のやり方は書かれておらず、コードを見るしかありませんでした。

```python
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException, status

def get_current_user(cred: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
    try:
        decoded_token = auth.verify_id_token(cred.credentials)
    except:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail='Invalid authentication credentials',
            headers={'WWW-Authenticate': 'Bearer'},
        )

    user = decoded_token['firebase']['identities']

    return user
```

正当なBearerトークンでないとアクセスできないエンドポイントを定義します。
FastAPIはDependsや型アノテーションで欲しいものをInjectできるので本当に便利です。

```python
from fastapi import FastAPI, Depends, HTTPException, status

app = FastAPI()

@app.get('/')
async def homepage(current_user=Depends(get_current_user)):
    return {'msg': 'ok', 'user': current_user}

if __name__ == '__main__':
    import uvicorn

    uvicorn.run(app, host='localhost', port=5000)
```

# 実行する

ここまでのコードをGistにまとめました。
まず以下のコードを用いてトークンを取得し、コピーします。
https://gist.github.com/NKNaaS/241687ecb5219ae0ce633a884d8ab5bb

次に以下のコードを用いてサーバーを起動します。
https://gist.github.com/NKNaaS/a698fd679fb545cb2cfe792f0114938c

それを適当なRESTクライアント(私は[Insomnia](https://insomnia.rest/)を使用)で叩きます。すると次のような結果が得られます。

```:request
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: insomnia/7.1.1
> Authorization: Bearer 
YOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKENYOURTOKEN
> Accept: */*
```

```json:response
{
  "msg": "ok",
  "user": {
    "identities": {
      "email": [
        "test001@example.com"
      ]
    },
    "sign_in_provider": "password"
  }
}
```

# 最後に

FastAPIもっと流行ってほしい。