RubyでFirebaseを利用して認証を行う

Rubyで [Firebase](https://firebase.google.com/docs/?hl=ja) で認証を行う実装のサンプルが見当たらなかったので業務で実装したものをまとめてみました。RubyでのFirebaseの活用例が増えれば幸いです。

## Firebaseでの認証
[Firebase Auth REST API](https://firebase.google.com/docs/reference/rest/auth/) を利用してユーザー登録、認証、セッショントークンの管理を行います。
Firebaseの `uid` からJWTを生成し、クライアントとの認証はJWTを介して行います。
`Firebase Auth REST API` のラッパーとして [firebase-auth](https://github.com/hungdh0x5e/firebase-auth) が便利だったので利用しています。

なお、Firebaseを利用した認証はセッションレスで常にJWTを介してリクエストを処理します。
トークンの有効期限が切れる毎に認証操作が発生するのを防ぐためにはクライアント側で発行されたJWTを保持する必要があります。

Firebaseのセッショントークンの有効期限は1時間です。
有効期限を過ぎたらリフレッシュトークンで新たにセッショントークンを生成する必要があります。

### 注意事項
`Firebase Auth REST API` は個人用(ログインユーザー)のAPIしか提供されていません。
例えば一部の不正行為をしたユーザーを利用停止するなどの操作がAPIでは実現できません(Firebaseコンソールからは可能)。
[公式のSDK](https://firebase.google.com/docs/reference/admin/) が提供されているNode.jsやGoといった言語は管理者用の操作も実行できます。
組織やグループ管理を扱うアプリケーションではFirebaseだけではユーザー管理に支障がでるので独自のユーザー管理の仕組を導入する必要があります。

## サンプルアプリケーションの説明
[firebase-auth-api](https://github.com/cobot00/firebase-auth-api) をGitHubで公開しているので参考にしてください。
RailsのAPIサーバーになります。

### 実装の特徴
ユーザー管理をアプリケーションで行えるようにするためにFirebaseのuidを直接利用するのではなく `users` テーブルを導入しています。
`users.deleted = true` のユーザーはアカウントが無効となり、認証に失敗します。
トークンの更新時には必ずFirebaseでの認証と `users.deleted` の確認を行い無効になったユーザーがアプリケーションを不正操作し続けるのを防いでいます。
なお、JWTは独自に生成しているので有効期限を設定しないことも可能ですが、セキュリティ上の大きなリスクを生じることになるのでFirebaseに合わせて有効期限を1時間にしています。

[Firebase::AuthClient](https://github.com/cobot00/firebase-auth-api/blob/master/app/models/firebase/auth_client.rb) がFirebaseへのリクエストを司どっています。
`Firebase::AuthClient` ラッパークラスで実際にリクエストを発行しているのは `firebase-auth` になります。
以下はメールアドレス・パスワードでの認証処理の実装の抜粋です。

```ruby
module Firebase
  class AuthResponse
    attr_accessor :uid, :id_token, :refresh_token

    def initialize(raw_response)
      @uid = raw_response.body['localId'] || raw_response.body['user_id']
      @id_token = raw_response.body['idToken'] || raw_response.body['id_token']
      @refresh_token = raw_response.body['refreshToken'] || raw_response.body['refresh_token']
    end
  end

  class AuthenticationException < StandardError; end

  class AuthClient
    def self.client
      Firebase::Auth::Client.new(FIREBASE::API_KEY)
    end

    def self.sign_in_email!(email, password)
      response = client.sign_in_email(email, password)
      unless response.success?
        message = ERROR_MESSAGES[response.body['error']['message']] || 'ログインに失敗しました'
        raise Firebase::AuthenticationException.new(message)
      end

      Firebase::AuthResponse.new(response)
    end
  end
end
```

### 動作環境・バージョン
Ruby: 2.5.1
Rails: 5.2.0

### セットアップ
`bundle install` 実行後に `users` テーブルを作成します。
なお、DBはPostgreSQLを前提にした実装になっているので他のDBを利用する場合は `config/initializers/constants.rb` の定数を書き換えてください。

```bash
bundle install
bin/rake db:migrate
```

#### Firebase
Firebaseプロジェクトで `ログイン方法` で `メール / パスワード` を有効にしてユーザーを事前に登録してください。

![firebase-authentication.png](https://qiita-image-store.s3.amazonaws.com/0/106217/1b04575d-da48-0ffa-17e7-0ae7a17cf8e7.png)

### 環境変数
以下の設定を環境変数で指定します。

|キー|値の例|説明|
|---|---|---|
|JWT_ALGORITHMS_TYPE|HS256|JWTの署名アルゴリズム|
|JWT_EXPIRATIONTIME|3600|セッショントークンの有効期間(秒)|
|JWT_ACTIVITY_API_SECRET_KEY|dummy|任意の文字列|
|FIREBASE_API_KEY|AIz...|FirebaseプロジェクトのAPIキー|

### API
以下のAPIを用意しています。
ユーザーの登録から、メールアドレス・パスワードでの認証、ユーザーの無効化が行えます。

|URI|メソッド|パラメーター|説明|
|---|:-:|---|---|
|/session|POST|メールアドレス、パスワード|認証、セッショントークンの生成|
|/session|PUT|-|セッショントークンのリフレッシュ|
|/user|POST|メールアドレス、パスワード、ユーザー名|ユーザーの登録|
|/user/:id|DELETE|-|ユーザーの無効化|
|/user/current|GET|-|ログインユーザーの情報を取得|

クライアントからのHTTPリクエストには以下の形式のHTTPヘッダーを付加してください。
`JWT` にはセッショントークンをセットします。
トークンのリフレッシュ処理(PUT /session)を呼び出す場合だけリフレッシュトークンをセットします。

```
Authorization: Bearer eyJ...(JWT)
```

`POST /user` でもセッショントークンを要求しているのは組織やグループで利用する想定だからです。
その場合にユーザー登録をするのはグループのリーダーや組織の管理者となります。
一般ユーザー向けのアプリケーションなら `POST /user` は認証無しで実行できるようにしてください。

#### curlでのコマンド例
動作検証のためのcurlコマンドのサンプルです。
最初に `POST /session` で認証し、セッショントークン・リフレッシュトークンを取得してください。
他のAPIはセッショントークン、またはリフレッシュトークンが必要になります。

#### 認証
```bash
curl -i -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"email": "user01@example.com","password": "dummy1234"}' http://localhost:3000/session
```

##### ユーザー登録
`Authorization` ヘッダーにはセッショントークンを設定します。

```bash
curl -i -H "Accept: application/json" -H "Content-type: application/json" -H "Authorization: Bearer eyJ..." -X POST -d '{"email": "user01@example.com","password": "dummy1234", "name":"user01"}' http://localhost:3000/user
```

##### ログインユーザーの情報取得
URIが `/user/:id` でない点に注意です。
ユーザーを特定するためのidはセッショントークンから取得するのが基本になります。

```bash
curl -i -H "Accept: application/json" -H "Content-type: application/json" -H "Authorization: Bearer eyJ..." http://localhost:3000/user/current
```

##### セッショントークンのリフレッシュ
`Authorization` ヘッダーにはリフレッシュトークンを設定します。

```bash
curl -i -H "Accept: application/json" -H "Content-type: application/json" -X PUT -H "Authorization: Bearer AK2..." http://localhost:3000/session
```