こんにちは、hachi8833です。
今回は、「Railsアプリの認証システムをセキュアにする4つの方法」でも取り上げられていたRodauthのREADMEを翻訳しました。
現時点では、残念ながらRodauthをRailsで使うためのroda-rails gemがRails 4.2までしか対応していないのと、ルーティングにDSLを使うことから、おそらくRails 5で使うには一手間かける必要がありそうです。
追記(2022/10/24)
roda-railsはその後アーカイブされました。
現在はjankoさんがrodauth-railsを精力的に開発しているのでこちらをチェックしてみてください。
しかし、認証のセキュリティを考えるうえで参考になる情報が多く、ドキュメントの質が(少なくともDeviseと比べて)非常に高いのが特徴です。大きな概要をまず示し、必要な概念も適宜示しながら、先に進むに連れて詳細に説明するという書き方が見事です。
さらに、READMEから読み取れる筋のよい認証システム設計も参考になると思います。パスワードハッシュの保存場所を完全に隔離した上で可能な限り自由度を高めている点や、機能ごとにカラムを足したり減らしたりするのではなくテーブルを足したり減らしたりする点など、学ぶところが多そうです。
概要
MITライセンスに基いて翻訳・公開します。
- リポジトリ: jeremyevans/rodauth
- 原文: README.rdoc
- 原文更新日: 2017/10/17
- 公式サイト: http://rodauth.jeremyevans.net/
Rodauth README(翻訳)
RodauthはRackアプリケーション向けの認証・アカウント管理フレームワークです。ビルドにはRodaとSequelを使っていますが、他のWebフレームワーク・データベースライブラリ・データベースでも利用できます。PostgreSQL・MySQL・Microsoft SQL Serverをデフォルト設定で使うと、データベース関数経由でのアクセスが保護されるようになり、パスワードハッシュのセキュリティを強化できます。
設計上のゴール
- セキュリティ: デフォルト設定で最大のセキュリティを利用できること
- 簡潔性: DSLで簡単に設定できること
- 柔軟性: フレームワークのどの部分でも機能をオーバーライドできること
機能リスト
- ログイン
- ログアウト
- パスワードの変更
- ログインの変更
- パスワードのリセット
- アカウントの作成
- アカウントの無効化
- アカウントのバリデーション
- パスワードの確認
- パスワードの保存(トークン経由での自動ログイン)
- ロックアウト(総当たり攻撃からの保護)
- OTP (TOTP経由の2要素認証)
- リカバリーコード(バックアップコード経由の2要素認証)
- SMSコード(SMS経由の2要素認証)
- ログイン変更のバリデーション(ログイン変更前の新規ログインバリデーション)
- アカウントの許容期間(ログイン前のバリデーションを不要にする)
- パスワードの許容期間(パスワードを最近入力した場合はパスワード入力を不要にする)
- パスワードの強度(より洗練されたチェック)
- パスワードの使い回し禁止
- パスワードの有効期限
- アカウントの有効期限
- セッションの有効期限
- シングルセッション(アカウントのアクティブセッションを1つに限定)
- JWT(他のすべての機能でJSON APIをサポート)
- パスワードハッシュの更新(ハッシュのcostが変更された場合)
- HTTP BASIC認証
リソース
- Webサイト
- http://rodauth.jeremyevans.net
- デモサイト
- http://rodauth-demo.jeremyevans.net
- ソースコード
- http://github.com/jeremyevans/rodauth
- バグ報告
- http://github.com/jeremyevans/rodauth/issues
- Google Group
- https://groups.google.com/forum/#!forum/rodauth
- IRC(チャット)
- irc://chat.freenode.net/#rodauth
依存関係
Rodauthがデフォルトで依存しているgemが若干ありますが、それらについてはRodauthの開発上依存しているものであり、運用上はgemなしで実行することも可能です。
- tilt、rack_csrf
- すべての機能で利用(JSON API onlyモードの場合を除く)
- bcrypt
- パスワードの一致チェックでデフォルトで利用(カスタム認証で
password_match?
をオーバーライドすればスキップ可能) - パスワードリセット時・アカウント確認時・ロックアウト機能のメール送信で利用
- rotp、rqrcode
- OTP機能で利用
- jwt
- JWT機能で利用
セキュリティ
データベース関数経由でのパスワードハッシュアクセス
RodauthでPostgreSQL・MySQL・Microsoft SQL Serverを利用する場合、デフォルトでパスワードハッシュにアクセスするためのデータベース関数を使います。これにより、アプリケーションを実行するユーザーはパスワードハッシュに直接アクセスできないようになっています。この機能によって攻撃者がパスワードハッシュにアクセスするリスクや、パスワードハッシュを他のサイトの攻撃に利用されるリスクを減らします。
本セクションでは以後この機能についてもっと詳しく説明します。なお、Rodauthはこの機能を使わなくても利用できます。異なるデータベースを利用している場合や、データベースの権限が不足している場合などには、この機能を利用できないことがあります。
パスワードはbcryptでハッシュ化され、アカウントテーブルとは別のテーブルに保存されます。また、2つのデータベース関数を追加します。1つはパスワードで使うsaltを取得する関数、もう1つは渡されたパスワードハッシュがユーザーのパスワードハッシュと一致するかどうかをチェックする関数です。
Rodauthでは2つのデータベースアカウントを使います。1つはアプリで使うアカウント用(以下「app
アカウント」)、もう1つはパスワードハッシュ用(以下「ph
アカウント」)です。ph
アカウントは、渡されたパスワードのsaltを取得するデータベース関数と、渡されたアカウントでパスワードハッシュが一致するかどうかをチェックする関数を設定します。2つの関数は、app
アカウントでph
アカウントのパーミッションを使って実行されるようになっています。これにより、app
アカウントでパスワードハッシュを読み取らずにパスワードをチェックできます。
app
アカウントではパスワードハッシュを読み出すことはできない代わりに、パスワードハッシュのINSERT、UPDATE、DELETEはできるので、この機能によって追加されたセキュリティで大きな不便は生じません。
app
アカウントでのパスワードハッシュ読み取りを禁止したことによって、仮にアプリのSQLインジェクションやリモートでのコード実行の脆弱性を攻撃された場合であっても、攻撃者によるパスワードハッシュの読み取りはさらに難しくなっています。
パスワードハッシュにこのようなセキュリティ対策を追加した理由は、弱いパスワードを使うユーザーやパスワードを使いまわすユーザーが後を絶たず、あるデータベースのパスワードハッシュが盗み出されると他のサイトのアカウントにまでアクセスされる可能性があるからです。そのため、たとえ保存されている他のデータの重要性が低いとしても、パスワードハッシュの保存方法はセキュリティ上きわめて重要度が高くなっています。
アプリのデータベースに機密情報が他にも保存されているのであれば、他の情報(あるいはすべての情報)についてもパスワードハッシュと同様のアプローチを行うことを検討すべきです。
トークン
アカウントの検証トークン、パスワードリセットのトークン、パスワード保存のトークン、ロックアウトのトークンでは同様のアプローチを採用しています。これらはすべてアカウントID_長いランダム文字列
形式のトークンを提供します。トークンにアカウントIDを含めることで、攻撃者は全アカウントに対してトークンを総当り(ブルートフォース)攻撃で推測できなくなり、一度に1人のユーザーに対してしか総当たり攻撃を行えなくなります。なお、トークンがランダム文字列だけで構成されていると、全アカウントに対して総当たり攻撃が可能になる場合があります。
さらに、トークンの比較にはタイミング攻撃に対して安全な関数を採用し、タイミング攻撃のリスクを低減しています。
PostgreSQLデータベースの設定
PostgreSQLでRodauthのセキュリティ設計をすべて利用するには、複数のデータベースアカウントを使います。
- データベースのsuperuserアカウント(通常はpostgres)
app
アカウント(実際はアプリと同じ名前にします)ph
アカウント(実際はアプリ名に_password
を追加した名前にします)
データベースのsuperuserアカウントは、データベースに関連する拡張(extension)の読み込みに使われます。アプリでは絶対にデータベースのsuperuserアカウントを使ってはいけません。
HerokuのPostgreSQLデータベースについては、上のようなシンプルな方法で複数のデータベースアカウントを設定する方法がありません。もちろん、HerokuでRodauthを使うことはできますが、セキュリティ上の利点は同じにはなりません。ただしこれはセキュリティ上危険ということではなく、パスワードハッシュの保存方法が他のメジャーな認証ソリューションと同じレベルになるということです。
データベースアカウントの作成
アプリがデータベースのsuperuserアカウントを使って実行されているのであれば、最初にapp
データベースアカウントの作成が必要です。このアカウント名はデータベース名と同じにしておくのが多くの場合ベストです。
続いてph
データベースアカウントを作成します。このアカウントはパスワードハッシュへのアクセスに使われます。
- PostgreSQLでの実行例
createuser -U postgres ${DATABASE_NAME}
createuser -U postgres ${DATABASE_NAME}_password
superuserアカウントがデータベース内の全アイテムの所有者になっている場合、上で作成したオーナーシップの変更が必要です。詳しくはhttps://gist.github.com/jeremyevans/8483320をご覧ください。
データベースの作成
一般に、アプリのアカウントはほとんどのテーブルを所有するので、アプリのアカウントがデータベースのオーナーとなります。
createdb -U postgres -O ${DATABASE_NAME} ${DATABASE_NAME}
上の方法はアプリ開発方法として最もセキュアとは言えないため、注意が必要です。セキュリティを最大化したい場合は、テーブルのオーナーとして独自のデータベースアカウントを使い、アプリのアカウントはテーブルのオーナーにならないようにし、正常動作に必要な最小限のアクセス権だけをアプリのアカウントに許可します。
拡張の読み込み
Rodauthのログイン機能で大文字小文字を区別しないログインをサポートするには、citext拡張を読み込む必要があります。
例:
psql -U postgres -c "CREATE EXTENSION citext" ${DATABASE_NAME}
Herokuの場合、citextは標準のデータベースアカウントで読み込まれます。ログインで大文字小文字を区別したいのであれば(ただし一般にはよくないとされています)、PostgreSQLのcitext拡張を読み込む必要はありません。その場合は、マイグレーション内のcitext
をString
に変更し、メールアドレスに対応できるようにしてください。
デフォルト以外のスキーマを使う
PostgreSQLは、デフォルトでパブリックなスキーマで新規テーブルをセットアップします。ユーザーごとに個別のスキーマを使いたい場合は、次のようにします。
psql -U postgres -c "DROP SCHEMA public;" ${DATABASE_NAME}
psql -U postgres -c "CREATE SCHEMA ${DATABASE_NAME} AUTHORIZATION ${DATABASE_NAME};" ${DATABASE_NAME}
psql -U postgres -c "CREATE SCHEMA ${DATABASE_NAME}_password AUTHORIZATION ${DATABASE_NAME}_password;" ${DATABASE_NAME}
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME} TO ${DATABASE_NAME}_password;" ${DATABASE_NAME}
psql -U postgres -c "GRANT USAGE ON SCHEMA ${DATABASE_NAME}_password TO ${DATABASE_NAME};" ${DATABASE_NAME}
スキーマを指定する拡張の読み込み部分のコードの変更が必要です。
psql -U postgres -c "CREATE EXTENSION citext SCHEMA ${DATABASE_NAME}" ${DATABASE_NAME}
ph
ユーザーでマイグレーションを実行する場合、スキーマ変更に対応するいくつかの変更が必要です。
create_table(:account_password_hashes) do
foreign_key :id, Sequel[:${DATABASE_NAME}][:accounts], :primary_key=>true, :type=>:Bignum
String :password_hash, :null=>false
end
Rodauth.create_database_authentication_functions(self, :table_name=>"${DATABASE_NAME}_password.account_password_hashes")
# if using the disallow_password_reuse feature:
create_table(:account_previous_password_hashes) do
primary_key :id, :type=>:Bignum
foreign_key :account_id, Sequel[:${DATABASE_NAME}][:accounts], :type=>:Bignum
String :password_hash, :null=>false
end
Rodauth.create_database_previous_password_check_functions(self, :table_name=>"${DATABASE_NAME}_password.account_previous_password_hashes")
また、次のRodauth設定メソッドを使って、アプリのアカウントが個別のスキーマで関数を呼び出すようにします。
function_name do |name|
"${DATABASE_NAME}_password.#{name}"
end
password_hash_table Sequel[:${DATABASE_NAME}_password][:account_password_hashes]
# disallow_password_reuse でパスワード再利用を禁止する場合:
previous_password_hash_table Sequel[:${DATABASE_NAME}_password][:account_previous_password_hashes]
MySQLデータベースの設定
MySQLにはオブジェクトの所有者という概念がなく、MySQLのGRANTやREVOKEのサポートはPostgreSQLと比べて限定されています。MySQLを使う場合、以下のようにph
アカウントにGRANT ALL
してすべてのパーミッションを与え、さらにWITH GRANT OPTION
でph
アカウントからapp
アカウントにGRANTできるようにすることをおすすめします。
CREATE USER '${DATABASE_NAME}'@'localhost' IDENTIFIED BY '${PASSWORD}';
CREATE USER '${DATABASE_NAME}_password'@'localhost' IDENTIFIED BY '${OTHER_PASSWORD}';
GRANT ALL ON ${DATABASE_NAME}.* TO '${DATABASE_NAME}_password'@'localhost' WITH GRANT OPTION;
マイグレーションの実行は常にph
アカウントで行い、app
アカウントには必要に応じてGRANTで特定のアクセス権を与えなければなりません。
MySQLでデータベース関数を追加するには、MySQLの設定にlog_bin_trust_function_creators=1
が必要になることがあります。
Microsoft SQL Serverデータベースの設定
Microsoft SQL Serverにはデータベースの所有者という概念はありますが、MySQLの場合と同様、ph
アカウントをデータベースのスーパーユーザーとして使い、ph
からGRANTでapp
アカウントにパーミッションを与えられるようにすることをおすすめします。
CREATE LOGIN rodauth_test WITH PASSWORD = 'rodauth_test';
CREATE LOGIN rodauth_test_password WITH PASSWORD = 'rodauth_test';
CREATE DATABASE rodauth_test;
USE rodauth_test;
CREATE USER rodauth_test FOR LOGIN rodauth_test;
GRANT CONNECT, EXECUTE TO rodauth_test;
EXECUTE sp_changedbowner 'rodauth_test_password';
マイグレーションの実行は常にph
アカウントで行い、app
アカウントには必要に応じてGRANTで特定のアクセス権を与えなければなりません。
テーブルの作成
異なる2種類のデータベースアカウントを使っているため、マイグレーションもデータベースアカウントごとに実行する必要があります(2つの異なるマイグレーションを実行します)。マイグレーションの例を以下に示します。このマイグレーションを変更して追加カラムをサポートしたり、Rodauthの不要な機能に関連するカラムやテーブルを削除することもできます。
1回目のマイグレーション
PostgreSQLの場合はapp
アカウントで実行する必要があります。MySQLやMicrosoft SQL Serverの場合はph
アカウントで実行する必要があります。
マイグレーションの実行にはSequel 4.35.0以降が必要です。これより前のバージョンのSequelを使っている場合は、:Bignum
シンボルをBignum
定数に変更してください。
Sequel.migration do
up do
extension :date_arithmetic
# アカウントの検証やアカウントの無効化機能で使用
create_table(:account_statuses) do
Integer :id, :primary_key=>true
String :name, :null=>false, :unique=>true
end
from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
db = self
create_table(:accounts) do
primary_key :id, :type=>:Bignum
foreign_key :status_id, :account_statuses, :null=>false, :default=>1
if db.database_type == :postgres
citext :email, :null=>false
constraint :valid_email, :email=>/^[^,;@ rn]+@[^,@; rn]+.[^,@; rn]+$/
index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
else
String :email, :null=>false
index :email, :unique=>true
end
end
deadline_opts = proc do |days|
if database_type == :mysql
{:null=>false}
else
{:null=>false, :default=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :days=>days)}
end
end
# パスワードのリセット機能で使用
create_table(:account_password_reset_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
DateTime :deadline, deadline_opts[1]
end
# アカウントの検証機能で使用
create_table(:account_verification_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
# ログイン変更の検証機能で使用
create_table(:account_login_change_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
String :login, :null=>false
DateTime :deadline, deadline_opts[1]
end
# パスワード保存機能で使用
create_table(:account_remember_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
DateTime :deadline, deadline_opts[14]
end
# ロックアウト機能で使用
create_table(:account_login_failures) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
Integer :number, :null=>false, :default=>1
end
create_table(:account_lockouts) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
DateTime :deadline, deadline_opts[1]
end
# パスワードの有効期限機能で使用
create_table(:account_password_change_times) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
DateTime :changed_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
# アカウントの有効期限機能で使用
create_table(:account_activity_times) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
DateTime :last_activity_at, :null=>false
DateTime :last_login_at, :null=>false
DateTime :expired_at
end
# シングルセッション機能で使用
create_table(:account_session_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
end
# OTP機能で使用
create_table(:account_otp_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :key, :null=>false
Integer :num_failures, :null=>false, :default=>0
Time :last_use, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
# リカバリーコード機能で使用
create_table(:account_recovery_codes) do
foreign_key :id, :accounts, :type=>:Bignum
String :code
primary_key [:id, :code]
end
# SMSコード機能で使用
create_table(:account_sms_codes) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :phone_number, :null=>false
Integer :num_failures
String :code
DateTime :code_issued_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
case database_type
when :postgres
user = get{Sequel.lit('current_user')} + '_password'
run "GRANT REFERENCES ON accounts TO #{user}"
when :mysql, :mssql
user = if database_type == :mysql
get{Sequel.lit('current_user')}.sub(/_password@/, '@')
else
get{DB_NAME{}}
end
run "GRANT ALL ON account_statuses TO #{user}"
run "GRANT ALL ON accounts TO #{user}"
run "GRANT ALL ON account_password_reset_keys TO #{user}"
run "GRANT ALL ON account_verification_keys TO #{user}"
run "GRANT ALL ON account_login_change_keys TO #{user}"
run "GRANT ALL ON account_remember_keys TO #{user}"
run "GRANT ALL ON account_login_failures TO #{user}"
run "GRANT ALL ON account_lockouts TO #{user}"
run "GRANT ALL ON account_password_change_times TO #{user}"
run "GRANT ALL ON account_activity_times TO #{user}"
run "GRANT ALL ON account_session_keys TO #{user}"
run "GRANT ALL ON account_otp_keys TO #{user}"
run "GRANT ALL ON account_recovery_codes TO #{user}"
run "GRANT ALL ON account_sms_codes TO #{user}"
end
end
down do
drop_table(:account_sms_codes,
:account_recovery_codes,
:account_otp_keys,
:account_session_keys,
:account_activity_times,
:account_password_change_times,
:account_lockouts,
:account_login_failures,
:account_remember_keys,
:account_login_change_keys,
:account_verification_keys,
:account_password_reset_keys,
:accounts,
:account_statuses)
end
end
2回目のマイグレーション
ph
アカウントで実行します。
require 'rodauth/migrations'
Sequel.migration do
up do
create_table(:account_password_hashes) do
foreign_key :id, :accounts, :primary_key=>true, :type=>:Bignum
String :password_hash, :null=>false
end
Rodauth.create_database_authentication_functions(self)
case database_type
when :postgres
user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
run "REVOKE ALL ON account_password_hashes FROM public"
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}"
when :mysql
user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
db_name = get(Sequel.function(:database))
run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
when :mssql
user = get(Sequel.function(:DB_NAME))
run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
end
# disallow_password_reuse 機能で使われる
create_table(:account_previous_password_hashes) do
primary_key :id, :type=>:Bignum
foreign_key :account_id, :accounts, :type=>:Bignum
String :password_hash, :null=>false
end
Rodauth.create_database_previous_password_check_functions(self)
case database_type
when :postgres
user = get(Sequel.lit('current_user')).sub(/_password\z/, '')
run "REVOKE ALL ON account_previous_password_hashes FROM public"
run "REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
when :mysql
user = get(Sequel.lit('current_user')).sub(/_password@/, '@')
db_name = get(Sequel.function(:database))
run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
when :mssql
user = get(Sequel.function(:DB_NAME))
run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
end
end
down do
Rodauth.drop_database_previous_password_check_functions(self)
Rodauth.drop_database_authentication_functions(self)
drop_table(:account_previous_password_hashes, :account_password_hashes)
end
end
マイグレーションを複数ユーザーで手分けして行いたい場合、SequelのマイグレーションAPIを使ってパスワードユーザーのマイグレーションを実行できます。
Sequel.extension :migration
Sequel.postgres('DATABASE_NAME', :user=>'PASSWORD_USER_NAME') do |db|
Sequel::Migrator.run(db, 'path/to/password_user/migrations', :table=>'schema_info_password')
end
PostgreSQL・MySQL・Microsoft SQL Server以外のデータベースを使う場合や、(データベースの)ユーザーアカウントを複数使えない場合は、単に2つのマイグレーションを1つのマイグレーションにまとめます。
上のマイグレーションを読むとわかるように、Rodauthでは1つのテーブルにさまざまなカラムを追加するのではなく、追加機能ごとにテーブルを追加する設計になっています。
使い方
基本的な使い方
RodauthはRodaのプラグインなので、以下のように他のRodaプラグインと同じ方法で読み込みます。
plugin :rodauth do
end
plugin
呼び出しでは、Rodauthの設定用DSLをブロックとして受け取ります。読み込む機能を指定するenable
という設定メソッドは省略できません。
plugin :rodauth do
enable :login, :logout
end
機能が読み込まれた後は、その機能でサポートされる設定用メソッドをすべて利用できるようになります。設定用メソッドには次の2種類があります。
-
- 認証系メソッド
1つ目は認証系メソッド(auth methods)と呼ばれます。これらのメソッドはブロックを1つ取り、Rodauthのデフォルトメソッドをオーバーライドします。ブロック内でsuper
を呼ぶとデフォルトの動作を取得できますが、super
には明示的に引数を渡す必要があります。なお、beforeフックやafterフックではsuper
を呼ぶ必要はありません。
たとえば、ユーザーのログイン時にログ出力を追加したい場合は次のようにします。
plugin :rodauth do
enable :login, :logout
after_login do
LOGGER.info "#{account[:email]} logged in!"
end
end
ブロック内は、リクエストに関連付けられたRodauth::Auth
インスタンスのコンテキストになります。このオブジェクトで次のメソッドを使うと、リクエストに関連するあらゆるデータにアクセスできます。
request
RodaRequest
のインスタンスresponse
RodaResponse
のインスタンスscope
Roda
のインスタンスsession
- セッションのハッシュ
flash
- flashメッセージのハッシュ
account
- アカウントモデルのインスタンス(Rodauthのメソッドで事前に設定済みの場合)
ログイン中のユーザーのIPアドレスをログ出力したい場合は次のようにします。
plugin :rodauth do
enable :login, :logout
after_login do
LOGGER.info "#{account[:email]} logged in from #{request.ip}"
end
end
-
- 認証値系メソッド
設定用メソッドの2つ目は認証値(auth value)のメソッドです。認証値系メソッドは認証系メソッドと似ていますが、単にブロックを受け取るほかに、ブロック無しで引数を1つ受け取ることもできます。受け取った引数は、その値を単に返すブロックとして扱われます。
たとえば、データベースのテーブルにアカウントを保存するaccounts_table
の場合、次のようにテーブル名をシンボルで渡すことでオーバーライドできます。
plugin :rodauth do
enable :login, :logout
accounts_table :users
end
認証値系メソッドはブロックを1つ受け取ることもできるので、リクエストから得られる情報を使ってすべての挙動を上書きできます。
plugin :rodauth do
enable :login, :logout
accounts_table do
request.ip.start_with?("192.168.1") ? :admins : :users
end
end
Rodauthではどの設定メソッドもブロックを受け取れるので、多くのレガシーシステムを統合するのに十分な柔軟性を備えています。
各機能のドキュメント
サポートされている各機能のオプションやメソッドについては、機能ごとに別ページを設けています。もしリンクが切れていたら、ドキュメントのディレクトリで必要なファイルを参照してください。
- Base(この機能は自動で読み込まれます)
- ログインでパスワードを必須にする: Base (この機能は、logins/passwordsを設定する機能によって自動で読み込まれます)
- メール: Base(この機能は、メール送信機能によって自動で読み込まれます)
- 2要素認証: Base(この機能は、2要素認証機能によって自動で読み込まれます)
- ログイン
- ログアウト
- パスワード変更
- ログイン変更
- パスワード変更
- アカウント作成
- アカウント無効化
- アカウント検証
- パスワード確認
- パスワード保存
- ロックアウト
- OTP
- リカバリーコード
- SMSコード
- ログイン変更の検証
- アカウント許容期間の検証
- パスワード許容期間
- パスワードの強度
- パスワード再利用の禁止
- パスワードの有効期限
- アカウントの有効期限
- セッションの有効期限
- シングルセッション
- JWT
Rodauthをルーティングツリーで呼び出す
一般に、以下のようにrodauth
をルーティングブロックの早い段階で呼び出すのが普通です。
route do |r|
r.rodauth
# ...
end
Rodauthはこれで実行できます。ただしこのままでは、アクセスするユーザーのログインを必須にしたり、サイトにセキュリティを追加したりできません。すべてのユーザーに対してログインを必須にするには、ログインしていないユーザーを以下のように強制的にログインページにリダイレクトします。
route do |r|
r.rodauth
rodauth.require_authentication
# ...
end
ログインを必須にしたいページがサイトの一部に限られている場合は、以下のようにすると、ルーティングツリーの特定のブランチについてだけユーザーがログインしていない場合にリダイレクトできます。
route do |r|
r.rodauth
r.on "admin" do
rodauth.require_authentication
# ...
end
# ...
end
Rodauthをルーティングツリーのルートではなく、ルーティングのブランチ内でだけ実行したい場合があります。その場合は以下のようにRodauthの設定で:prefix
を設定してから、ルーティングツリーの該当するブランチでr.rodauth
を呼び出します。
plugin :rodauth do
enable :login, :logout
prefix "auth"
end
route do |r|
r.on "auth" do
r.rodauth
end
rodauth.require_authentication
# ...
end
rodauth
メソッド
Rodauthの機能のほとんどはr.rodauth
経由で公開されています。これを使って、Rodauthで自分が有効にした機能にルーティングできます(ログイン機能の/login
など)。しかし、上述したようにこうしたメソッドをrodauth
オブジェクトで呼び出したいこともあります(現在のリクエストが認証済みであるかどうかのチェックなど)。
以下のメソッドは、r.rodauth
の外でもrodauth
オブジェクトで呼び出せるように設計されています。
require_login
- セッションでログインを必須にし、ログインしていないリクエストをログインページにリダイレクトします。
require_authentication
require_login
と似ていますが、アカウントが2要素認証用に設定されている場合は2要素認証も必須にします。ログイン済みであっても2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。logged_in?
- セッションがログイン中であるかどうかを返します。
authenticated?
logged_in?
と似ていますが、アカウントが2要素認証用に設定されている場合はセッションが2要素認証されているかどうかを返します。require_two_factor_setup
- (2要素認証用)セッションで2要素認証を必須にします。2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。
uses_two_factor_authentication?
- (2要素認証用)現在のセッションのユーザーが2要素認証を使えるよう設定されているかどうかを返します。
update_last_activity
- (アカウント有効期限用)現在のアカウントの最終活動時刻を更新します。最終活動時刻を基にアカウントの有効期限が切れるようにしてある場合にのみ意味があります。
require_current_password
- (アカウント有効期限用)アカウントのパスワードの有効期限が切れた場合に、パスワード変更ページにリダイレクトして現在のパスワードを入力しないと継続できないようにします。
load_memory
- (パスワード保存機能用)セッションが認証されていない場合に、remember cookieがあるかどうかをチェックします。有効なremember cookieがある場合はセッションに自動ログインしますが、rememberキー経由でログインしたというマークを付けます。
logged_in_via_remember_key?
- (パスワード保存機能用) rememberキーを使って現在のセッションにログインしたかどうかを返します。セキュリティ上重要な操作でパスワードの再入力を必須にしたい場合は、
confirm_password
を使えます。 check_session_expiration
- (セッション有効期限用) 現在のセッションの有効期限が切れているかどうかをチェックし、期限切れの場合は自動的にログアウトします。
check_single_session
- (シングルセッションの有効期限) 現在のセッションがまだ有効かどうかをチェックし、無効な場合はセッションからログアウトします。
verified_account?
- (許容期間の確認の延長) 現在のアカウントが(訳注: メールなどで)確認済みかどうかを返します。
false
の場合、ユーザーが「許容期間」に該当しているためにログインを許されていることを示します。 locked_out?
- (ロックアウト機能) 現在のセッションのユーザーがロックアウトされているかどうかを返します。
複数の設定を使う
Rodauthでは、同じアプリケーションで複数のrodauth設定の利用をサポートしています。これは、プラグインを読み込んで2度目のログインで別設定の名前を指定するだけで行なえます。
plugin :rodauth do
end
plugin :rodauth, :name=>:secondary do
end
その後は、いつでもルーティングでrodauth
を呼び、使いたい設定名を引数で指定できるようになります。
route do |r|
r.on 'secondary' do
r.rodauth(:secondary)
end
r.rodauth
end
パスワードハッシュをアカウントのテーブルに保存する
Rodauthでは、パスワードハッシュをアカウントと同じテーブルに保存することもできます。これは、パスワードハッシュを保存するカラムを指定するだけで行なえます。
plugin :rodauth do
account_password_hash_column :password_hash
end
Rodauthでこのオプションを設定すると、パスワードハッシュのチェックをRubyで行うようになります。
PostgreSQL/MySQL/Microsoft SQL Serverでデータベース関数を使わないようにする
RodauthとPostgreSQL/MySQL/Microsoft SQL Serverで、認証用のデータベース関数を使いたくないがハッシュテーブルは従来どおり別テーブルに保存したい場合は、次のようにします。
plugin :rodauth do
use_database_authentication_functions? false
end
言い換えると、rodauth_get_salt
関数とrodauth_valid_password_hash
関数を独自に実装すれば、PostgreSQL/MySQL/Microsoft SQL Server以外のデータベースでもこの値をtrue
にできます。
認証をカスタマイズする
Rodauthの設定用メソッドの中には、他の種類の認証方法を使えるようにできるものもあります。
認証をカスタマイズすると、ログインの変更やパスワードの変更などのRodauthの機能の使い方がわからなくなったり、カスタム設定を追加する必要が生じたりするかもしれません。ただし以下のカスタマイズ例では、ログイン機能とログアウト機能は正常に機能します。
- LDAP認証を使う
アカウントがデータベースに保存されている状態でLDAP認証したい場合は、simple_ldap_authenticator
ライブラリを利用できます。
require 'simple_ldap_authenticator'
plugin :rodauth do
enable :login, :logout
require_bcrypt? false
password_match? do |password|
SimpleLdapAuthenticator.valid?(account.username, password)
end
end
データベースにアカウントがない状態でLDAPの有効なユーザーがログインできるようにしたい場合は、次のようにします。
require 'simple_ldap_authenticator'
plugin :rodauth do
enable :login, :logout
# LDAPで認証するのでbcryptライブラリをrequireしない
require_bcrypt? false
# セッションの値を:loginキーに保存する
# (デフォルトの:account_idキーだとわかりにくいため)
session_key :login
# セッションの値で与えられたログインを使う
account_session_value{account}
# このログインそのものをアカウントとして使う
account_from_login{|l| l.to_s}
password_match? do |password|
SimpleLdapAuthenticator.valid?(account, password)
end
end
- Facebook認証を使う
JSON APIでのFacebook認証の例を以下に示します。この設定では、クライアント側にJSONでPOSTリクエストを送信するコードがあることが前提です。このPOSTリクエストは/login
に送信され、FacebookでユーザーのOAuthアクセストークンを設定するaccess_token
パラメータを含むとします。
require 'koala'
plugin :rodauth do
enable :login, :logout, :jwt
require_bcrypt? false
session_key :facebook_email
account_session_value{account}
login_param 'access_token'
account_from_login do |access_token|
fb = Koala::Facebook::API.new(access_token)
if me = fb.get_object('me', :fields=>[:email])
me['email']
end
end
# there is no password!
password_match? do |pass|
true
end
end
- その他のWebフレームワーク
Rodauthは、アプリケーションでRoda Webフレームワークが使われていなくても利用できます。これは、Rodauthを使うRodaミドルウェアを追加することで行なえます。
require 'roda'
class RodauthApp < Roda
plugin :middleware
plugin :rodauth do
enable :login
end
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
end
end
use RodauthApp
RodauthはRodaアプリに対し、Rodaがレイアウト提供の目的で使われることを期待します。そのため、Rodauthを他のアプリ用のミドルウェアとして使う場合、Rodauthから使えるviews/layout.erb
ファイルがないのであれば、おそらくRodaのrender
プラグインの追加も必要になります。その場合、Rodauthがアプリと同じレイアウトを使えるようプラグインを適切に設定する必要もあるでしょう
ミドルウェア内部のルーティングブロックでenv['rodauth'] = rodauth
を設定すると、Rodauthメソッドを簡単に呼び出せる方法をアプリに導入できるようになります。
Rodaを使わないアプリでのRodauth導入例をいくつか示します。
- Ginatra: SinatraベースのGitリポジトリビューア
- Rodauthのデモ(Railsアプリ)(ここではRailsのCSRFやflashサポートをRodauthから使えるようにするためにroda-rails gemを使っています)
- Grapeアプリ
- Hanamiアプリ
-
2要素認証を使う
Rodauthでは、TOTP(Time-Based One-Time Passwords: RFC 6238)経由での2要素認証を使えます。Rodauthで2要素認証をアプリに統合する方法は、アプリでの必要に応じてさまざまなものがあります。
2要素認証のサポートはOTP機能の一部なので、ログイン機能に加えてOTP機能も有効にする必要があります。一般に、2要素認証を実装する場合は2要素認証を2種類用意し、プライマリの2要素認証が利用できない場合にセカンダリの2要素認証を提供するべきです。RodauthではSMSコードとリカバリーコードをセカンダリ2要素認証としてサポートします。
アプリで2要素認証をサポートし、かつ2要素認証を必須にしたくない場合は次のようにします。
plugin :rodauth do
enable :login, :logout, :otp, :recovery_codes, :sms_codes
end
route do |r|
r.rodauth
rodauth.require_authentication
# ...
end
OTP認証を全ユーザーで必須にし、アカウントを持っていないユーザーに対してOTP認証の設定を要求する場合の設定です。
route do |r|
r.rodauth
rodauth.require_authentication
rodauth.require_two_factor_authentication_setup
# ...
end
認証を必須にする場合の一般的な方法と同様に、特定のブランチでのみ2要素認証を必須にし、サイトの他の場所ではログイン認証を必須することもできます。
route do |r|
r.rodauth
rodauth.require_login
r.on "admin" do
rodauth.require_two_factor_authenticated
end
# ...
end
JSON APIサポート
プラグインに:json
オプションを渡してJWT機能を有効にすると、JSONレスポンス取り扱いのサポートを追加できます。
plugin :rodauth, :json=>true do
enable :login, :logout, :jwt
end
JSON APIをビルドするのであれば、:json => :only
を渡すことでRodauthで通常読み込まれるHTML関連のプラグイン(render、csrf、flash、h)を読み込まないようにできます。
plugin :rodauth, :json=>:only do
enable :login, :logout, :jwt
end
ただし、メール送信機能はデフォルトでrenderプラグインに依存していることにご注意ください。:json=>:only
を使う場合は、renderプラグインを手動で読み込むか、*_email_body
設定オプションでメールの本文を指定する必要があります。
JWT機能を導入すると、Rodauthに含まれるその他のJSON APIサポートもすべて利用できるようになります。
rodauth
オブジェクトにカスタムメソッドを追加する
設定のブロック内でauth_class_evalを使うと、
rodauth`オブジェクトから呼び出せるカスタムメソッドを追加できます。
plugin :rodauth do
enable :login
auth_class_eval do
def require_admin
request.redirect("/") unless account[:admin]
end
end
end
route do |r|
r.rodauth
r.on "admin" do
rodauth.require_admin
end
end
外部の機能を使う
有効にする設定メソッドは、Rodauthの外部にある機能を読み込めます。この外部機能のファイルは、rodauth/features/feature_nameからrequireできるディレクトリに置く必要があります。このファイルは以下の基本構造をとる必要があります。
module Rodauth
# :feature_nameは、有効にしたい機能を指定する引数
# :FeatureNameはオプションで、inspect出力を読みやすくする定数名を設定するのに使う
Feature.define(:feature_name, :FeatureName) do
# 認証値系メソッドを固定値で定義するショートカット
auth_value_method :method_name, 1 # method_value
auth_value_methods # 認証値メソッドごとに1つの引数
auth_methods # 認証メソッドごとに1つの引数
route do |r|
# この機能のルーティングへのリクエストをこのブロックで受ける
# ブロックはRodauth::Authインスタンスのスコープで評価される
# rはリクエストのRoda::RodaRequestインスタンス
r.get do
end
r.post do
end
end
configuration_eval do
# メソッド固有の追加設定を必要に応じてここで定義する
end
# auth_methodsとauth_value_methodsのデフォルトの挙動を定義する
# ...
end
end
機能の構成方法の例については、Rodauthの機能のコードを参照してください。
ルーティングレベルの挙動をオーバーライドする
Rodauthのすべての設定メソッドは、Rodauth::Auth
インスタンスの挙動を変更します。しかし場合によってはルーティング層の扱いをオーバーライドしたくなることもあります。これは、r.rodauth
を呼び出す前に以下のように適切なルーティングを追加するだけで簡単に行なえます。
route do |r|
r.post 'login' do
# ここにカスタム POST /login ハンドリングを記述する
end
r.rodauth
end
Rodauthテンプレートをプリコンパイルする
Rodauthは自分自身のgemフォルダにあるテンプレートを使います。fork型のWebサーバーを使っていて、コンパイル済みテンプレートを事前に読み込んでメモリを節約したい場合や、アプリをchrootしたい場合は、Rodauthのテンプレートをプリコンパイルすることでメリットを得られます。
plugin :rodauth do
# ...
end
precompile_rodauth_templates
0.9.xからのアップグレード
Rodauthを0.9.xから現在のバージョンにアップグレードする場合の注意点です。
account_valid_password
データベース関数を使っていた場合はこれを削除し、上のマイグレーションに記載されている2つのデータベース関数を追加する必要があります。以下のコードをマイグレーションに追加することでこの作業を行えます。
require 'rodauth/migrations'
run "DROP FUNCTION account_valid_password(int8, text);"
Rodauth.create_database_authentication_functions(self)
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO ${DATABASE_NAME}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO ${DATABASE_NAME}"
類似のプロジェクト
以下はすべてRailsに特化しています。
- Devise
- Authlogic
- Sorcery
著者
Jeremy Evans (code@jeremyevans.net)
追記(2022/11/11)
本記事の更新版は以下です。