Rails における BASIC 認証について調べたのでその記録です。
シンプルなケース
https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic.html にあるように http_basic_authenticate_with
を使うのが最もシンプルな実装でしょう。
class PostsController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", 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
end
ただし、これだと一組の name, password しか設定できません。
発展的なケース
業務上の理由で、複数の name, password の組に対してBASIC認証を設定したいシーンがありました。
改めて https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic.html を読んでみると、"Advanced Basic example" として次のコードが紹介されていました。
class ApplicationController < ActionController::Base
before_action :set_account, :authenticate
private
def set_account
@account = Account.find_by(url_name: request.subdomains.first)
end
def authenticate
case request.format
when Mime[:xml], Mime[:atom]
if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
@current_user = user
else
request_http_basic_authentication
end
else
if session_authenticated?
@current_user = @account.users.find(session[:authenticated][:user_id])
else
redirect_to(login_url) and return false
end
end
end
end
リクエストの format が xml か atom のときに BASIC 認証を要求しているようですね。
authenticate_with_http_basic
と request_http_basic_authentication
が初見でした。
authenticate_with_http_basic
について
https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic を読んでみると
def authenticate_with_http_basic(&login_procedure)
HttpAuthentication::Basic.authenticate(request, &login_procedure)
end
とありました。なるほど HttpAuthentication::Basic.authenticate
ってなんですか。
https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic.html#method-i-authenticate ですね。
def authenticate(request, &login_procedure)
if has_basic_credentials?(request)
login_procedure.call(*user_name_and_password(request))
end
end
「BASIC 認証の認証情報があるなら login_procedure
を call
するぜ (ブロックとして渡された認証手続きを実行するぜ)」と言ってますね。
「そのブロックに渡す引数は *user_name_and_password
な!」とも言ってます。
user_name_and_password
はその名の通り BASIC 認証で入力された user_name と password ですね。slat演算子 *
がついてることからも配列だと分かります。コードはこちらの通り https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic.html#method-i-user_name_and_password
def user_name_and_password(request)
decode_credentials(request).split(":", 2)
end
ここまでの情報をもとに、サンプルコードのこれ↓がなにをやっているのかを整理すると
if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
@current_user = user
else
end
authenticate_with_http_basic { |u, p| ... }
- リクエストにBASIC認証情報があるならば、その user_name と password (それぞれ変数
u
, p
)をつかってブロック内のなんらかの処理をする
- リクエストにBASIC認証情報がないならば
nil
を返す
if user = ...
- 上記の処理が真ならば(user に何らかの真な値が入るならば)
@current_user = user
という感じですね。
request_http_basic_authentication
について
https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-request_http_basic_authentication を読んでみると
def request_http_basic_authentication(realm = "Application", message = nil)
HttpAuthentication::Basic.authentication_request(self, realm, message)
end
とのこと。はーい、 HttpAuthentication::Basic.authentication_request
ってなんですか
https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic.html#method-i-authentication_request
def authentication_request(controller, realm, message)
message ||= "HTTP Basic: Access denied.\n"
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
controller.status = 401
controller.response_body = message
end
401 を返して BASIC 認証を要求しているってことですね。
BASIC 認証自体については HTTP 認証 - HTTP | MDN を参照のこと。
さて、改めてサンプルコードに戻ると
if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
@current_user = user
else
request_http_basic_authentication
end
authenticate_with_http_basic
が偽だったら BASIC 認証を要求する、ということですね。
これらを使って複数の user_name, password の BASIC 認証を実装してみる
class ApplicationController < ActionController::Base
before_action :authenticate
private
def authenticate
credentials = [%w[name1 password1], %w[name2 password2]]
unless authenticate_with_http_basic { |u, p| credentials.include?([u, p]) }
request_http_basic_authentication
end
end
end
こんな感じですかねぇ
さらに便利メソッドがある
この記事を書きながら調べてたら authenticate_or_request_with_http_basic
とかいうメソッドを見つけました。
名前からしてそうって感じなんですが https://api.rubyonrails.org/v7.0.4.2/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_or_request_with_http_basic を読むと
def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
end
ということで、さっきまで初見だった2つのメソッドを組み合わせたメソッドなんですね。
なので、↑で実装したコードを書き換えると
class ApplicationController < ActionController::Base
before_action :authenticate
private
def authenticate
credentials = [%w[name1 password1], %w[name2 password2]]
authenticate_or_request_with_http_basic { |u, p| credentials.include?([u, p]) }
end
end
と書けそうですね。動作確認してないので動かなかったら教えて下さい。
24時を過ぎて数分経ってしまいましたが、これは1月の記事です。さっきお茶をこぼさなければ絶対に24時前に書き終えられたはずなんです、だれですかあんなところにお茶を置いたのは!
おやすみなさい 🍵
追記: 投稿日過去にできました。 https://i-am-an-easy-going.hatenablog.com/entry/2020/02/16/225916