LoginSignup
62
50

More than 5 years have passed since last update.

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

Posted at

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

適応ケース

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

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

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

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

ドキュメントを眺める

ActionController::HttpAuthentication を見てみると分かるんですが、 ActionController とかいうやつの中に Basic, Digest, Token という Module が存在していてこいつを取り入れるだけでほぼほぼ実装が終わります。

実際にサンプルコードを見るとこんな感じで利用します。

 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 に書かれている通りコントローラーに Module を追加する必要があります。

上記サンプルに Module を追加したのが下記のコードです。

 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 にしています。

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 に切り替えています。

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 を利用します。

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

使い方

トークン発行

$ bundle exec rails runner -e development 'Token.create(label: "システム名", key: "secret")'

Request

$ curl -H "Authorization: Bearer secret" http://localhost:3000/debug

まとめ

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

62
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
50