PythonからFirebase認証を利用する(メール/パスワード認証編)

絶賛 Firebase 調査中です。
これまで [「Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)」](https://qiita.com/masatomix/items/5316acdb5980289e5cc2) では、Webアプリ(つまりフロントエンド側)をFirebase認証で保護する方法を整理しました。
つづいて[「Cloud Functions で構築したREST API、自前RESTサーバのAPIにFirebase認証を適用する。」](https://qiita.com/masatomix/items/b446a07a641f97686a97) こちらでは、FirebaseのFunctionsや自前のRESTサーバ(つまりバックエンド側)をFirebase認証で保護する方法を整理しました。

さて今回は、普通のアプリ(つまりWeb系じゃないフロントエンド側)をFirebase認証で保護する方法です。Pythonでアプリを開発してて、さくっと認証したいってニーズがあり、Firebaseの「メール/パスワード認証」を使えるかな？と試したときのメモです。

結論いうと、簡単かつ、とてもつかいやすいです :-)

ちなみに、メール/パスワード認証だけでなく、Firebase経由のOAuth認可機構なども、利用できることを確認してるんですが、それはまたあらためて別に書くことにします。


## 前提の環境

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.4 (Mojaveです)
BuildVersion:	18E226

$ python --version
Python 3.7.1
```

Pythonのお手前にのっとって、分離環境をつくります。

```bash
$ python3 -m venv ~/venv/venv_fb
$ source ~/venv/venv_fb/bin/activate
(venv_fb) $ which python3
/Users/xxx/venv/venv_fb/bin/python3
(venv_fb) $ 
```

アプリ側の環境構築は以上です。

## Firebase認証(メール/パスワード認証)を有効にする

Firebase プロジェクトの構築はいろんなヒトが記事にしているので、そこは省略。。
いわゆる

```javascript
  var config = {
    apiKey: "## FIREBASE API KEY ###",
    authDomain: "### FIREBASE AUTH DOMAIN ###",
    databaseURL: "https://##PROJECT ID##.firebaseio.com",
    projectId: "### CLOUD FIRESTORE PROJECT ID ###",
    storageBucket: "##PROJECT ID##.appspot.com",
    messagingSenderId: "YOUR-SENDER-ID"
  };
```
とかまでは、できてる前提ですすめます。

さて認証機能のセットアップです。
[Firebaseのコンソール](https://console.firebase.google.com/u/0/?hl=ja)から、自分が作成したプロジェクトを選択し、左のメニュー部の「Authentication」を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/e62a5031-0754-e15d-d35a-bc211cbb207e.png)

「ログイン方法」を選択すると、下記のようにログイン方法を選択する画面になります。今回は「メール/パスワード認証」をつかうので、メール/パスワードを「有効」にしておきましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/43c0bdb2-5818-b97e-3a24-0eb2ef91fba6.png)


さてメール/パスワード認証するためのユーザを作っておきます。

|アドレス|パスワード|
|-----------------|------------------|
|test001@example.com| hogehogehoge|

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/f37cd3a6-c436-60ef-8202-0cfcf98130a1.png)

Firebase側の準備も以上です。


## やってみる

さて早速やってみますが [Firebase Authentication](https://firebase.google.com/docs/auth/) のサイトを見ると、認証のSDKはPython版が存在しないようですね。なので、[Firebase Auth REST API](https://firebase.google.com/docs/reference/rest/auth/#section-sign-in-email-password) にあるREST APIを直接コールすることになります。

まあ REST APIのインタフェース見ながら値をセットすればOK[^1]です。。


あ、じつは[このへん](https://firebase.google.com/docs/database/rest/start?hl=ja)みると、サードパーティのRESTのWrapperがあったりするんですが、今回はシンプルに直接コールしてみたいとおもいます。

[^1]: 電文を見てみると他の言語のSDKも、おなじRESTをコールしてるだけっぽいですね。

### コード

下記の通りにコードを準備します。

```python:config/config.ini
(venv_fb) $ cat config/config.ini
[firebase_config]
  api_key = ## FIREBASE API KEY ###  // Firebaseの apiKey

[proxy]
  proxy = no
  http = http://127.0.0.0:8888
  https = http://127.0.0.0:8888
(venv_fb) $ 
```

```python:index.py
(venv_fb) $ cat index.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
import requests
import configparser

def main():
    config = configparser.ConfigParser()
    config.read("./config/config.ini")

    api_key = config["firebase_config"]["api_key"]

    email = 'test001@example.com'
    password = 'hogehogehoge'

    user = sign_in_with_email_and_password(api_key, email, password, config)

    print('---- sign_in_with_email_and_password -----')
    print_pretty(user)
    # print(user['idToken'])


def sign_in_with_email_and_password(api_key, email, password, config):
    """
    Firebaseで認証を行う(SDKの signInWithEmailAndPassword と同値)
    :param api_key:
    :param email:
    :param password:
    :return: Firebaseの、idTokenなどを含んだ、認証情報
    """
    # https://firebase.google.com/docs/reference/rest/auth/#section-sign-in-email-password
    uri = f"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={api_key}"
    headers = {"Content-type": "application/json"}
    data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
    proxies, verify = get_proxy(config)

    result = requests.post(url=uri,
                           headers=headers,
                           data=data,
                           proxies=proxies,
                           verify=verify)
    return result.json()


def get_proxy(config):
    verify = True
    proxies = None

    if config["proxy"].getboolean("proxy"):
        proxies = {
            "http": config["proxy"]["http"],
            "https": config["proxy"]["https"]
        }
        verify = False

    return proxies, verify

def print_pretty(obj):
    print(json.dumps(obj, ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ': ')))


if __name__ == '__main__':
    main()
```

電文の中身をチェックするためにproxyをかませられるようにしてあったりしますが、RESTのAPIをよんでるだけのシンプルなコードです。

### 実行する

コードは requests ライブラリを使用しているので、pip でインストールしつつ、実行してみます。

```bash
(venv_fb)$ pip install requests
...
(venv_fb)$
(venv_fb)$ python3 index.py

---- sign_in_with_email_and_password -----
{
    "displayName": "",
    "email": "test001@example.com",
    "expiresIn": "3600",
    "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkMmY5ZjNmYjgzZDYzMzc0OTdiNmY3Y2QyY2ZmNGRmYTVjMmU4YjgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20veHh4eHgteHh4eHgiLCJhdWQiOiJ4eHh4eC14eHh4eCIsImF1dGhfdGltZSI6MTU1NDgxMjAxMSwidXNlcl9pZCI6IlV2M2ZuNkpSdHljM1pid05XYXh4eHh4eHgiLCJzdWIiOiJVdjNmbjZKUnR5YzNaYndOV2F4eHh4eHh4IiwiaWF0IjoxNTU0ODEyMDExLCJleHAiOjE1NTQ4MTU2MTEsImVtYWlsIjoidGVzdDAwMUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0MDAxQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0K.wEa67EK-zVLwERkkxiXpkJjH5pHsRlOv4qC74R9ICh25I0oEXHGBFfnYvL2HPPYKP3X5YF-wL-FYjejPa138bjoAJ57hfRgW-5Gceu0GBNeDPIiim-jifNQ3K4AQzqak9WbUYChNPnzjW_vuRUscKAgTZIMz6ozYmv3UC-whDhKcBP8IMqVGKXlX2Z6-4X4eEfPQ2-gKQ_fiXhpEO9sTRV3KOahCKtkpFrtdoHCcSpOeoATdb-Z_h71FXVi-R6oiVG5F5AAObUVY0nhGqZfzCZl6WG2J8Fl5UG5_7oaTFIcGFE2jyLqvnph63buGRsLU0rMku8kX1p3f4v8JPWLWpw",
    "kind": "identitytoolkit#VerifyPasswordResponse",
    "localId": "Uv3fn6JRtyc3ZbwNWaAjxXowkwc2",
    "refreshToken": "AEu4IL0tZ5IsoBGlqHaw8Ll_5IQ5vcY_sEAYDqecAD7dbBR7oBYp93oCG-z_ajhcNWlJtwAc52Ub5vhckWBT6VyS6qSOSs0z403qKlw8YH4UtiGZ9-aXuOT8VT4n8_B26hTOl_YBQPgG9ZqPSbPDv3yuy78ZZQVP0syLVuQLGJxJzbB9YzogGQRSgDPrd2etIHkA4tignV99oR8eAIdQFAuNhpEmjWkm-Q",
    "registered": true
}
(venv_fb)$
```

何やら結果が表示されましたが、上記の形式の戻り電文が返ってくれば、認証OKといえそうです。ちなみにわざとパスワードを間違えてみると、

```json
{
    "error": {
        "code": 400,
        "errors": [
            {
                "domain": "global",
                "message": "INVALID_PASSWORD",
                "reason": "invalid"
            }
        ],
        "message": "INVALID_PASSWORD"
    }
}
```
という電文が返ってきました。詳細のインタフェース仕様は [Firebase Auth REST API](https://firebase.google.com/docs/reference/rest/auth/#section-sign-in-email-password) をご参照ください。


ログイン認証するだけでよければ、メール/パスワードによるFirebase認証は以上です。おつかれさまでした。

## 戻り値に含まれる idToken について

さて認証するだけであれば以上で完了ですが、対向する自前のRESTサーバのAPIなどもおなじくFirebase認証で保護したい場合は、戻り電文に含まれているidTokenを用いることになるので、それをもうすこしだけみてみます。

ちなみにidTokenとは、上記の

```bash
eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkMmY5ZjNmYjgzZDYzMzc0OTdiNmY3Y2QyY2ZmNGRmYTVjMmU4YjgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20veHh4eHgteHh4eHgiLCJhdWQiOiJ4eHh4eC14eHh4eCIsImF1dGhfdGltZSI6MTU1NDgxMjAxMSwidXNlcl9pZCI6IlV2M2ZuNkpSdHljM1pid05XYXh4eHh4eHgiLCJzdWIiOiJVdjNmbjZKUnR5YzNaYndOV2F4eHh4eHh4IiwiaWF0IjoxNTU0ODEyMDExLCJleHAiOjE1NTQ4MTU2MTEsImVtYWlsIjoidGVzdDAwMUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0MDAxQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0K.wEa67EK-zVLwERkkxiXpkJjH5pHsRlOv4qC74R9ICh25I0oEXHGBFfnYvL2HPPYKP3X5YF-wL-FYjejPa138bjoAJ57hfRgW-5Gceu0GBNeDPIiim-jifNQ3K4AQzqak9WbUYChNPnzjW_vuRUscKAgTZIMz6ozYmv3UC-whDhKcBP8IMqVGKXlX2Z6-4X4eEfPQ2-gKQ_fiXhpEO9sTRV3KOahCKtkpFrtdoHCcSpOeoATdb-Z_h71FXVi-R6oiVG5F5AAObUVY0nhGqZfzCZl6WG2J8Fl5UG5_7oaTFIcGFE2jyLqvnph63buGRsLU0rMku8kX1p3f4v8JPWLWpw
```

のことです。


### idTokenをほどいて中身を見てみる

このidTokenは [前の記事](https://qiita.com/masatomix/items/b446a07a641f97686a97#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%80%A4%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)で調べたとおり[JWT(JSON Web Tokens)](https://jwt.io/) で符号化されているので、ドットで区切ってbase64で戻してみます。

ドットで区切ったうちの、まずひとつめ。

```bash:(ココだけlinuxで叩いてます。微妙にオプションとか違うかも)
$ echo eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkMmY5ZjNmYjgzZDYzMzc0OTdiNmY3Y2QyY2ZmNGRmYTVjMmU4YjgiLCJ0eXAiOiJKV1QifQ | base64 -d  | jq
{
  "alg": "RS256",
  "kid": "7d2f9f3fb83d6337497b6f7cd2cff4dfa5c2e8b8",
  "typ": "JWT"
}
```

なんだかJWTとか鍵のアルゴリズムRS256とかが書いてあります。そしてふたつめはこちら。

```bash:(ココだけlinuxで叩いてます。微妙にオプションとか違うかも)
$ echo eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20veHh4eHgteHh4eHgiLCJhdWQiOiJ4eHh4eC14eHh4eCIsImF1dGhfdGltZSI6MTU1NDgxMjAxMSwidXNlcl9pZCI6IlV2M2ZuNkpSdHljM1pid05XYXh4eHh4eHgiLCJzdWIiOiJVdjNmbjZKUnR5YzNaYndOV2F4eHh4eHh4IiwiaWF0IjoxNTU0ODEyMDExLCJleHAiOjE1NTQ4MTU2MTEsImVtYWlsIjoidGVzdDAwMUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJ0ZXN0MDAxQGV4YW1wbGUuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0K | base64 -d | jq
{
  "iss": "https://securetoken.google.com/xxxxx-xxxxx",
  "aud": "xxxxx-xxxxx",
  "auth_time": 1554812011,
  "user_id": "Uv3fn6JRtyc3ZbwNWaxxxxxxx",
  "sub": "Uv3fn6JRtyc3ZbwNWaxxxxxxx",
  "iat": 1554812011,
  "exp": 1554815611,
  "email": "test001@example.com",
  "email_verified": false,
  "firebase": {
    "identities": {
      "email": [
        "test001@example.com"
      ]
    },
    "sign_in_provider": "password"
  }
}
```

このように、いろいろ取り出すことができました。このidTokenは、JWTの署名によって改ざんされていないことがチェックできたり、

- aud(Audience) の値で、発行したFirebaseプロジェクトの値を確認
- exp(Expiration time)の値によって、有効期限が切れたトークンでないかを確認

などができます。したがってこのidTokenを自前のRESTサーバに送ることで、APIをFirebase認証で保護するように作り込むことができるわけですね。

その辺の詳細は、下記に整理しましたのでご参考にどうぞ。

参考: [RESTサーバから、トークンの正当性チェックを行いたい](https://qiita.com/masatomix/items/b446a07a641f97686a97#functions%E3%81%8B%E3%82%89%E3%81%A7%E3%81%AA%E3%81%8F%E8%87%AA%E5%89%8D%E3%81%AErest%E3%82%B5%E3%83%BC%E3%83%90%E3%81%8B%E3%82%89%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E6%AD%A3%E5%BD%93%E6%80%A7%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%82%92%E8%A1%8C%E3%81%84%E3%81%9F%E3%81%84)


## まとめ
メール/パスワード認証による、普通のアプリ(つまりWeb系じゃないフロントエンド)の認証機能の実装は以上です。ひきつづき、普通のアプリをFirebase経由のOAuthをつかって保護する方法をまとめてみたいと思います。色々調べてみた感じだと、OAuthの処理シーケンスがブラウザのリダイレクト前提(いわゆるOAuth Dance!) なため、ローカルでWEBサーバを立ち上げて認可コードがリダイレクトされてくるのを待つ、みたいなやり方でいちおう実現できそうです、、。このへんはこちらのかたの情報を参考にさせてもらいました。

[PythonでCLIからOAuth2を利用してQiitaのアクセストークンを取得してみた](https://qiita.com/kai_kou/items/d03abd6012f32071c1aa)

あらためて、記事にしてみます。

おつかれさまでした。

## 関連リンク

- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2)  WEBフロントエンド側をFirebase認証で保護する方法を整理
- [Cloud Functions で構築したREST APIをFirebase認証で保護する。そして自前RESTサーバのAPIにもFirebase認証を適用する。](https://qiita.com/masatomix/items/b446a07a641f97686a97) バックエンド側をFirebase認証で保護する方法を整理
- [PythonでCLIからOAuth2を利用してQiitaのアクセストークンを取得してみた](https://qiita.com/kai_kou/items/d03abd6012f32071c1aa) PythonからOAuth/OIDC をつかう方法で参考にさせてもらいました。
- [Authleteを使った認可サーバの構築手順(CIBA対応版)](https://qiita.com/masatomix/items/9598b55c2658fb963120) CIBAが導入されて、リダイレクトなOAuth Danceがなくなれば、ローカルでWEBサーバを立ち上げるとか不要になるかな。。(Pythonでポーリングするだけになれば楽)
- [JWT(JSON Web Tokens)](https://jwt.io/)  JWT公式