認証付きの 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
まとめ
システム間の認証ならこんな感じでもいいかなーという一例なので、こうやったらどうでしょうなどあればコメントください。