Rails APIのトークン認証おもったより楽だったので紹介

認証付きの API を作る必要があったので調べていてこんな感じで実装したっていう共有記事です。


# 適応ケース

全体像は、ブラウザ → BFF → バックエンド という感じのアプリを想定しています。

今回紹介する方法の利用用途としては BFF → バックエンド間のプライベートAPIといった感じです。Token を発行して万が一漏れたら更新するという流れになります。

BFF → ブラウザ間は Cookie と Session とかでよしなにやったほうが良いと思います。
理由は、ブラウザ側でアクセストークンの管理とかからおさらばできて、SPA とかで API 叩くときに Cookie つけて送るだけで認証通ってたらレスポンス返ってくるし通ってなかったらレスポンス帰ってこないからのエラーハンドリングするだけになるからです。

OAuthとかと連携する場合も BFF 側もしくはリバースプロキシでセッション管理するべきでしょう。(最近はそう思っているだけで、数カ月後には意見が変わっている可能性があります)

# ドキュメントを眺める

[ActionController::HttpAuthentication](http://api.rubyonrails.org/classes/ActionController/HttpAuthentication.html) を見てみると分かるんですが、 ActionController とかいうやつの中に `Basic`, `Digest`, `Token` という Module が存在していてこいつを取り入れるだけでほぼほぼ実装が終わります。

 実際にサンプルコードを見るとこんな感じで利用します。
 
 ```ruby
 class PostsController < ApplicationController
  TOKEN = "secret"

  before_action :authenticate, except: [ :index ]

  def index
    render plain: "Everyone can see me!"
  end

  def edit
    render plain: "I'm only accessible if you know the password"
  end

  private
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        # Compare the tokens in a time-constant manner, to mitigate
        # timing attacks.
        ActiveSupport::SecurityUtils.secure_compare(
          ::Digest::SHA256.hexdigest(token),
          ::Digest::SHA256.hexdigest(TOKEN)
        )
      end
    end
end
```

しかしAPI mode の場合動きません。
理由は単純で `ActionController::API` には `ActionController::HttpAuthentication` Module は含まれていません。
[railsguides](https://railsguides.jp/api_app.html#%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B) に書かれている通りコントローラーに Module を追加する必要があります。
 
上記サンプルに Module を追加したのが下記のコードです。

```diff
 class PostsController < ApplicationController
+ include ActionController::HttpAuthentication::Token::ControllerMethods

  TOKEN = "secret"

  before_action :authenticate, except: [ :index ]

  def index
    render plain: "Everyone can see me!"
  end

  def edit
    render plain: "I'm only accessible if you know the password"
  end

  private
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        # Compare the tokens in a time-constant manner, to mitigate
        # timing attacks.
        ActiveSupport::SecurityUtils.secure_compare(
          ::Digest::SHA256.hexdigest(token),
          ::Digest::SHA256.hexdigest(TOKEN)
        )
      end
    end
end
```

ヘッダーに `Authorization: Token secret` もしくは `Authorization: Bearer secret` を付与して実行すると `token` に secret が渡されます。

# 実行コード

## マイグレーション

同じlabelは生成したくないので unique にしています。
同じhashは生成されないと思いますが、念のため unique にしています。

```ruby
class CreateTokens < ActiveRecord::Migration[5.1]
  def change
    create_table :tokens do |t|
      t.string :label, null: false
      t.string :digest_hash, null: false

      t.index :label, unique: true
      t.index :digest_hash, unique: true
      t.timestamps
    end
  end
end
```

## モデル

key を before_save で digest_hash に切り替えています。

```ruby
class Token < ApplicationRecord
  attr_accessor :key

  validates :key, presence: true
  validates :label, presence: true
  before_save :rewrite_digest_hash

  def self.authenticate?(token)
    exists?(digest_hash: Digest::SHA512.hexdigest(token))
  end

  private

  def rewrite_digest_hash
    self.digest_hash = Digest::SHA512.hexdigest(key)
  end
end
```

## コントローラー

`authenticate_or_request_with_http_token` だとレスポンスが自由に返せないので `authenticate_with_http_token` を利用します。

```ruby
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  before_action :authenticate

  private

  def authenticate
    authenticate_token || render_unauthorized
  end

  def authenticate_token
    authenticate_with_http_token do |token|
      Token.authenticate?(token)
    end
  end

  def render_unauthorized
    render json: { message: 'token invalid' }, status: :unauthorized
  end
end
```
 
# 使い方
 
## トークン発行
 
```bash
$ bundle exec rails runner -e development 'Token.create(label: "システム名", key: "secret")'
```
 
## Request
 
 
```bash
$ curl -H "Authorization: Bearer secret" http://localhost:3000/debug
```
 
# まとめ

システム間の認証ならこんな感じでもいいかなーという一例なので、こうやったらどうでしょうなどあればコメントください。