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
について
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 96 def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(request, &login_procedure) end
とありました。なるほど HttpAuthentication::Basic.authenticate
ってなんですか。
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 105 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
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 115 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| ... }
if user = ...
- 上記の処理が真ならば(user に何らかの真な値が入るならば)
@current_user = user
- current_user に user を入れる
という感じですね。
request_http_basic_authentication
について
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 100 def request_http_basic_authentication(realm = "Application", message = nil) HttpAuthentication::Basic.authentication_request(self, realm, message) end
とのこと。はーい、 HttpAuthentication::Basic.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 を読むと
# File actionpack/lib/action_controller/metal/http_authentication.rb, line 92 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