更新履歴
更新前のREADME翻訳は以下です。
Ruby: 認証gem「Rodauth」README(更新翻訳)
RodauthはRubyで最も高度な認証システムであり、任意のRackアプリケーションで動作します。RodaとSequelで構築されていますが、他のWebフレームワーク、データベースライブラリ、データベースでも利用できます。
PostgreSQL、MySQL、Microsoft SQL Serverをデフォルト設定で使うと、データベース関数経由でのアクセスが保護されるようになり、パスワードハッシュのセキュリティを強化できます。
Rodauthでは、さまざまな種類の多要素認証手法およびパスワードレス認証手法をサポートし、サポートされているどの機能についてもHTML APIおよびJSON APIを提供します。
🔗 設計上のゴール
- セキュリティ: デフォルト設定で最大のセキュリティを利用できること
- 簡潔性: DSLで簡単に設定できること
- 柔軟性: フレームワークのどの部分でも機能をオーバーライドできること
🔗 機能リスト
- ログイン
- ログアウト
- パスワードの変更
- ログインの変更
- パスワードのリセット
- アカウントの作成
- アカウントの無効化
- アカウントのバリデーション
- パスワードの確認
- パスワードの保存(トークン経由での自動ログイン)
- ロックアウト(総当たり攻撃からの保護)
- 監査ログの出力
- メール認証(メールリンク経由のパスワードレスログイン)
- WebAuthn(WebAuthn経由の多要素認証)
- WebAuthnログイン(WebAuthn経由のパスワードレスログイン)
- WebAuthnアカウント検証(パスワードレスWebAuthnセットアップ)
- WebAuthn Autofill (ログイン時にWebAuthn credentialsをオートフィルする)
- OTP(TOTP経由の多要素認証)
- リカバリーコード(バックアップコード経由の多要素認証)
- SMSコード(SMS経由の多要素認証)
- ログイン変更のバリデーション(ログイン変更前の新規ログインバリデーション)
- アカウントの猶予期間(ログイン前のバリデーションを不要にする)
- パスワードの猶予期間(パスワードを最近入力した場合はパスワード入力を不要にする)
- パスワードの強度(より洗練されたチェック)
- パスワード"pepper"
- パスワードの再利用禁止
- ありふれたパスワードの禁止
- パスワードの有効期限
- アカウントの有効期限
- セッションの有効期限
- アクティブセッション(ログアウト後のセッション再利用防止、全セッションのログアウトの許可)
- シングルセッション(アカウントのアクティブセッションを1つに限定)
- JSON(他のあらゆる機能でJSON APIをサポート)
- JWT(他のあらゆる機能でJSON Web Tokenをサポート)
- JWTリフレッシュ(トークンのアクセスとリフレッシュ)
- JWT CORS(Cross-Origin Resource Sharing)
- パスワードハッシュの更新(ハッシュのcostが変更された場合)
- Argon2
- HTTP BASIC認証
- パスワード変更時の通知
- パスワードリセット時の通知
- 内部リクエスト
- パスのクラスメソッド(
*_path
や*_url
)
🔗 リソース
- Webサイト
- http://rodauth.jeremyevans.net
- デモサイト
- http://rodauth-demo.jeremyevans.net
- ソースコード
- http://github.com/jeremyevans/rodauth
- バグ報告
- http://github.com/jeremyevans/rodauth/issues
- GitHubフォーラム
- https://github.com/jeremyevans/rodauth/discussions
- 別のフォーラム(Google Group)
- https://groups.google.com/forum/#!forum/rodauth
依存関係
Rodauthはデフォルトでいくつかのgemに依存しています。これらのgemはなくても実行できるので、ランタイム依存ではなく開発への依存です。
- tilt
- すべての機能で利用(JSON API onlyモードの場合や、プラグインの
:render=>false
オプションを使う場合を除く) - rack_csrf
- csrf: :rack_csrfプラグインオプションが渡された場合のCSRFサポートで利用(デフォルトはRodaのroute_csrfですが、rack_csrfはより安全なリクエスト固有トークンを利用できます)
- bcrypt
- パスワードの一致チェックでデフォルトで利用(カスタム認証で
password_match?
をオーバーライドすればスキップ可能) - argon2
- パスワードハッシュ生成用bcryptの代替機能argon2で利用
- メールの
reset_password
、verify_account
、verify_login_change
、change_password_notify
、lockout
、email_auth
でデフォルトで利用 - rotp、rqrcode
- OTP機能で利用
- jwt
- JWT機能で利用
- webauthn
- WebAuthn機能で利用
🔗 セキュリティ
🔗 データベース関数経由でのパスワードハッシュアクセス
RodauthでPostgreSQL、MySQL、Microsoft SQL Serverを利用する場合は、デフォルトでデータベース関数を利用してパスワードハッシュにアクセスします。これにより、アプリケーションを実行するユーザーはパスワードハッシュに直接アクセスすることが不可能になります。この機能によって攻撃者がパスワードハッシュにアクセスするリスクや、パスワードハッシュを他のサイトの攻撃に利用されるリスクを減らします。
本セクションでこの後、この機能についてもっと詳しく説明します。なお、Rodauthはこの機能を使わなくても利用できます。他のデータベースを利用している場合や、データベースの権限が不足している場合などには、この機能を利用できないことがあります。
パスワードはデフォルトでbcryptによってハッシュ化され、アカウントテーブルとは別のテーブルに保存されます。また、2つのデータベース関数を追加します。1つはパスワードで使うsaltを取得する関数、もう1つは渡されたパスワードハッシュがユーザーのパスワードハッシュと一致するかどうかをチェックする関数です。
Rodauthでは2つのデータベースアカウントを使います。1つはアプリで使うアカウント用(以下app
アカウント)で、このapp
アカウントはパスワードハッシュの読み取りアクセス権を持ちません。もう1つはパスワードハッシュ用(以下ph
アカウント)です。ph
アカウントは、渡されたパスワードのsaltを取得するデータベース関数と、渡されたアカウントでパスワードハッシュが一致するかどうかをチェックする関数を設定します。2つの関数は、app
アカウントでph
アカウントのパーミッションを用いて実行されます。これにより、app
アカウントでパスワードハッシュを読み取らずにパスワードをチェックできます。
app
アカウントはパスワードハッシュを読み出せませんが、代わりにパスワードハッシュのINSERT、UPDATE、DELETEは可能なので、この機能によって追加されたセキュリティで大きな不便は生じません。
app
アカウントでのパスワードハッシュ読み取りを禁止したことによって、仮にアプリのSQLインジェクションやリモートでのコード実行の脆弱性を攻撃された場合であっても、攻撃者によるパスワードハッシュの読み取りはさらに難しくなります。
パスワードハッシュにこのようなセキュリティ対策を追加した理由は、弱いパスワードを使うユーザーやパスワードを使いまわすユーザーがあとを絶たず、あるデータベースのパスワードハッシュが盗み出されると他のサイトのアカウントにまでアクセスされる可能性があるからです。そのため、たとえ保存されている他のデータの重要性が低いとしても、パスワードハッシュの保存方法はセキュリティ上きわめて重要度が高くなっています。
アプリのデータベースに機密情報が他にも保存されているのであれば、他の情報(あるいはすべての情報)についてもパスワードハッシュと同様のアプローチを行うことを検討すべきです。
🔗 トークン
「アカウント検証」「パスワードのリセット」「メール認証」「ログイン変更の検証」「パスワード保存(remember me)」「ロックアウト」のトークンでは、すべて同様のアプローチを採用しています。
これらはすべてアカウントID_長いランダム文字列
形式のトークンを提供します。トークンにアカウントIDを含めることで、攻撃者は全アカウントに対してトークンを総当り(ブルートフォース)攻撃で推測できなくなり、一度に1人のユーザーに対してしか総当たり攻撃を行えなくなります(なお、トークンがランダム文字列だけで構成されていると、全アカウントに対して総当たり攻撃が可能になる場合があります)。
さらに、トークンの比較にはタイミング攻撃に対して安全な関数を採用し、タイミング攻撃のリスクを低減しています。
🔗 HMAC
Rodauthは後方互換性のためデフォルトではHMACを利用しませんが、hmac_secret
設定メソッドを用いてHMAC secretを利用することが強く推奨されています。以下で説明するように、HMAC secretを設定するとセキュリティが強化されます。
🔗 email_base
の機能
この機能は、メール送信のすべての機能で利用されます。hmac_secret
を設定すると、メール送信されるトークンではHMACを利用し、データベースに保存される生のトークンではHMACを使いません。これにより、SQLインジェクションの脆弱性などによってデータベース内のトークンが漏洩したとしても、hmac_secret
にアクセスしなければトークンを利用できなくなります。HMACが有効になっていないと、生のトークンがメール送信され、データベース内から漏洩したトークンでアクセス可能になってしまいます。
HMACへの移行を緩やかに進められるように、allow_raw_email_token
を一時的にtrueに設定できます。こうすることで、以前送信したメールに含まれる生のトークンが引き続き利用可能になります。ただしhmac_secret
で追加されるセキュリティが失われてしまうため、あくまで一時的な設定として用いるべきです。
ほとんどのメール送信機能では、デフォルトのトークンが1日で失効しますが、例外的にverify_account
機能のトークンは次の理由で失効しないようになっています。verify_account
でユーザーがhmac_secret
を設定するより前に、allow_raw_email_token
が無効になった状態でメール送信をリクエストすると、検証メールの再送信をリクエストする必要が生じますが、そのときにHMACを用いるメールを受信することになります。
🔗 パスワード保存機能
email_base機能と同様に、パスワード保存(remember me)機能でもHMACを利用してトークンをcookieに記憶し、データベースには生のトークンを保存します。これにより、データベース内の生のトークンが漏洩したとしても、hmac_secret
を知らなければ記憶トークンは利用不可能になります。
raw_remember_token_deadline
設定メソッドを使うと、記憶トークンのdeadlineが指定時間より前になっている場合に、生の記憶トークンを利用できるようになります。これにより、記憶トークンでHMACを使うための移行を緩やかに進められるようになります。デフォルトのdeadlineはトークン作成後14日後なので、このデフォルト設定を使っている場合は、記憶トークンのHMACを有効にした日の14日後に設定する必要があります。
🔗 OTP(ワンタイムパスワード)機能
hmac_secret
を設定すると、ユーザーにはHMAC化されたOTPキーを送信し、データベースには生のOTPキーを保存します。これによって、データベース内の生のトークンが漏洩したとしても、hmac_secret
の知識がなければ2要素認証を利用できません。
残念ながら、この機能については既存のユーザーを緩やかに移行するシンプルな方法はありません。
ユーザーが既にOTP機能を利用しているRodauthインストールにhmac_secret
を導入する場合、以下のいずれかを行わなければなりません。
- 既存のOTPキーをすべて失効させて置き換える
otp_keys_use_hmac?
をfalseに設定して生のOTPキーを引き続き利用するotp_keys_use_hmac?
を上書きして、hmac_secret
がコンフィグに追加される前の時期に生成されたOTPキーについてはfalseを返し、そうでない場合はtrueを返す
デフォルトのotp_keys_use_hmac?
は、hmac_secret
が設定されている場合はtrueに、それ以外の場合はfalseに設定されます。
otp_keys_use_hmac?
がtrueの場合は、OTPのセットアップ時にOTPが確実にサーバーによって生成されたことも確認します。
otp_keys_use_hmac?
がfalseの場合は、フォーマットが有効な任意のOTPキーをセットアップで受け付けます。
otp_keys_use_hmac
がtrueで、かつJWTとOTPの機能を使用中で、かつJSONリクエスト経由でOTPをセットアップする場合は、最初にOTPセットアップのルーティングにPOSTリクエストを送信する必要があります。そのときに、JSONのotp_secret
パラメータとotp_raw_secret
パラメータでエラーが返されます。OTPをセットアップするには、これらのパラメータをotp_secret
の有効なOTP認証コードとともにPOSTリクエストで送信する必要があります。
🔗 WebAuthn機能
WebAuthn機能を使うにはhmac_secret
の設定が必須です。理由は、提供された認証チャレンジが変更されていないことを確認するのにhmac_secret
が使われるからです。
🔗 アクティブセッション機能
active_sessions機能を使うにはhmac_secret
の設定が必須です。理由は、データベースにアクティブセッションIDのHMACが保存されるからです。
🔗 シングルセッション機能
hmac_secret
を設定すると、セッションに設定される単一のセッションsecretがHMAC化されるようになります。
セッション自体は少なくともHMACで保護されているので(暗号化されていない場合)、これはセキュリティには影響しません。これは単に、データベース内の生のトークンがユーザーに提供されるトークンと区別される形で一貫性を保つことが目的です。allow_raw_single_session_key?
をtrueに設定することで移行を緩やかに行えるようになります。
🔗 PostgreSQLデータベースの設定
PostgreSQLでRodauthのセキュリティ設計をすべて利用するには、複数のデータベースアカウントを使います。
- データベースのsuperuserアカウント(通常はpostgres)
app
アカウント(実際はアプリと同じ名前にします)ph
アカウント(実際はアプリ名に_password
を追加した名前にします)
データベースのsuperuserアカウントは、データベースに関連する拡張(extension)の読み込みに使われます。アプリでは絶対にデータベースのsuperuserアカウントを使ってはいけません。
🔗 データベースアカウントの作成
アプリがデータベースのsuperuserアカウントで実行される場合は、最初にapp
データベースアカウントの作成が必要です。このアカウント名をデータベース名と同じにしておくのが、多くの場合ベストの方法です。
続いてph
データベースアカウントを作成します。このアカウントはパスワードハッシュへのアクセスに使われます。
- PostgreSQLでの実行例
createuser -U postgres ${DATABASE_NAME}
createuser -U postgres ${DATABASE_NAME}_password
superuserアカウントがデータベース内の全アイテムの所有者になっている場合、上で作成したオーナーシップの変更が必要です。詳しくはGistをご覧ください。
🔗 データベースの作成
一般に、アプリのアカウントはほとんどのテーブルを所有するので、アプリのアカウントがデータベースのオーナーとなります。
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 15以降)
PostgreSQL 15では、デフォルトのデータベースセキュリティが変更され、パブリックスキーマへの書き込みアクセス権限は「データベース所有者」にのみ与えられるようになりました。Rodauthは、セットアップの際にph
アカウントがパブリックスキーマへの書き込みアクセス権限を持っていることを前提としています。
このアクセス権限を一時的に付与するには、以下を実行します(マイグレーション完了後にこの権限を取り消してください)。
psql -U postgres -c "GRANT CREATE ON SCHEMA public TO ${DATABASE_NAME}_password" ${DATABASE_NAME}
🔗 デフォルト以外のスキーマを使う
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: Sequel[:${DATABASE_NAME}_password][:account_password_hashes])
# disallow_password_reuse機能を使う場合:
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: Sequel[:${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.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: /^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
else
String :email, null: false
end
if db.supports_partial_indexes?
index :email, unique: true, where: {status_id: [1, 2]}
else
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
# 監査ログイン機能で利用
json_type = case database_type
when :postgres
:jsonb
when :sqlite, :mysql
:json
else
String
end
create_table(:account_authentication_audit_logs) do
primary_key :id, type: :Bignum
foreign_key :account_id, :accounts, null: false, type: :Bignum
DateTime :at, null: false, default: Sequel::CURRENT_TIMESTAMP
String :message, null: false
column :metadata, json_type
index [:account_id, :at], name: :audit_account_at_idx
index :at, name: :audit_at_idx
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]
DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# jwtリフレッシュ機能で利用
create_table(:account_jwt_refresh_keys) do
primary_key :id, type: :Bignum
foreign_key :account_id, :accounts, null: false, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[1]
index :account_id, name: :account_jwt_rk_account_id_idx
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
DateTime :email_last_sent, 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]
DateTime :email_last_sent
end
# メール認証機能で利用
create_table(:account_email_auth_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
DateTime :deadline, deadline_opts[1]
DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP
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
end
# シングルセッション機能で利用
create_table(:account_session_keys) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :key, null: false
end
# アクティブセッション機能で利用
create_table(:account_active_session_keys) do
foreign_key :account_id, :accounts, type: :Bignum
String :session_id
Time :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
primary_key [:account_id, :session_id]
end
# WebAuthn機能で利用
create_table(:account_webauthn_user_ids) do
foreign_key :id, :accounts, primary_key: true, type: :Bignum
String :webauthn_id, null: false
end
create_table(:account_webauthn_keys) do
foreign_key :account_id, :accounts, type: :Bignum
String :webauthn_id
String :public_key, null: false
Integer :sign_count, null: false
Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP
primary_key [:account_id, :webauthn_id]
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 SELECT, INSERT, UPDATE, DELETE ON account_statuses TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON accounts TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_authentication_audit_logs TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_reset_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_jwt_refresh_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_verification_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_change_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_remember_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_failures TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_email_auth_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_lockouts TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_change_times TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_activity_times TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_session_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_active_session_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_user_ids TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_keys TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_recovery_codes TO #{user}"
run "GRANT SELECT, INSERT, UPDATE, DELETE ON account_sms_codes TO #{user}"
end
end
down do
drop_table(:account_sms_codes,
:account_recovery_codes,
:account_otp_keys,
:account_webauthn_keys,
:account_webauthn_user_ids,
:account_session_keys,
:account_active_session_keys,
:account_activity_times,
:account_password_change_times,
:account_email_auth_keys,
:account_lockouts,
:account_login_failures,
:account_remember_keys,
:account_login_change_keys,
:account_verification_keys,
:account_jwt_refresh_keys,
:account_password_reset_keys,
:account_authentication_audit_logs,
:accounts,
:account_statuses)
end
end
🔗 2回目のマイグレーション
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個のテーブルにさまざまなカラムを追加するのではなく、追加機能ごとにテーブルを追加する設計になっています。
🔗 スキーマ権限の取り消し(PostgreSQL 15以降)
マイグレーションを実行する前にパブリックスキーマへのアクセス権限を明示的に付与した場合は、以下を実行して、付与したアクセス権限をマイグレーション実行後に取り消してください。
psql -U postgres -c "REVOKE CREATE ON SCHEMA public FROM ${DATABASE_NAME}_password" ${DATABASE_NAME}
🔗 ロックダウン機能(PostgreSQLのみ)
マイグレーションの実行後にph
アカウントがデータベースに直接ログインできないようにしておくと、セキュリティを若干向上させることができます。これはpg_hba.confファイルを変更することで実現できます。また、アクセスをGRANTやREVOKEで制限することを検討してもよいでしょう。
データベース自体へのアクセスをapp
アカウントのみに制限できます。このアカウントはデータベースを所有しているので、app
アカウントを用いて以下を実行できます。
GRANT ALL ON DATABASE ${DATABASE_NAME} TO ${DATABASE_NAME};
REVOKE ALL ON DATABASE ${DATABASE_NAME} FROM public;
また、publicスキーマへのアクセスも制限できます(カスタムスキーマを使う場合は不要)。デフォルトではデータベースのスーパーユーザーがpublicスキーマを所有するので、以下はデータベースのスーパーユーザーアカウント(通常はpostgres
)として実行する必要があります。
GRANT ALL ON SCHEMA public TO ${DATABASE_NAME};
GRANT USAGE ON SCHEMA public TO ${DATABASE_NAME}_password;
REVOKE ALL ON SCHEMA public FROM public;
MySQLやMicrosoft SQL Serverを使う場合は、ph
アカウントで直接ログインできないようにアクセス制限する方法をそれぞれのドキュメントで参照してください。
🔗 使い方
🔗 基本的な使い方
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. 認証値系メソッド
設定用メソッドの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ではどの設定メソッドもブロックを受け取れるので、多くのレガシーシステムを統合するのに十分な柔軟性を備えています。
🔗 プラグインのオプション
Rodauthプラグインを読み込むときに、どの依存プラグインを読み込むかを指定するオプションハッシュを渡すこともできます。以下のオプションを利用できます。
:csrf
false
に設定するとcsrfプラグインを読み込まなくなります。route_csrfプラグインではなくcsrfプラグインを使う場合は:rack_csrf
に設定します。:flash
false
に設定するとflashプラグインを読み込まなくなります。:render
false
に設定すると、renderプラグインを読み込まなくなります。これは、別のビューライブラリを使う場合にtiltへの依存関係を回避するのに有用です。:json
true
に設定するとjsonプラグインとjson_parserプラグインを読み込みます。:only
に設定すると、他のプラグインは読み込まずにこれらのプラグインだけを読み込みます。ただし、メール送信機能を有効にしている場合は、renderプラグインを手動で読み込む必要があります。:name
- Rodauth設定の名前を指定します。この設定は、Rodaアプリケーションで複数のRodauth設定をサポートするのに使われます。
:auth_class
- Rodaアプリケーションで設定すべき特定の
Rodauth::Auth
サブクラスを指定します。デフォルトでは無名のRodauth::Auth
サブクラスが作成されます。
🔗 各機能のドキュメント
サポートされている各機能のオプションやメソッドについては、機能ごとに別ページを設けています。もしリンクが切れていたら、ドキュメントのディレクトリで必要なファイルを参照してください(訳注: 利便性のためドキュメントへのリンクと概要を追加しました)。
- Base
- 他の機能で共有される機能(自動読み込み)。
- ログインでパスワードを必須にする: Base
- ログインやパスワード設定機能で共有される機能(自動読み込み)。
- メール: Base
- メール送信機能で共有される機能(自動読み込み)。
- 2要素認証: Base
- 2要素認証機能で共有される機能(自動読み込み)。
- アカウントの有効期限
- 指定の期間を過ぎてもログインや操作がなかった場合にアカウントへのアクセスを無効にする。
- アクティブセッション
- ログアウト後のセッション再利用を禁止し、そのアカウントのすべてのセッションをグローバルにログアウトする。
- 監査ログ出力
- rodauthの全操作をデータベーステーブルに監査出力する
- Argon2
- パスワードのハッシュアルゴリズムにargon2を利用する。
- ログイン変更
- ユーザーが自分のログインを変更できるようにする。
- パスワード変更
- ユーザーが自分のパスワードを変更できるようにする。
- パスワード変更の通知
- 「パスワード変更」機能でパスワードを変更した場合にメールでユーザーに通知する。
- アカウント閉鎖
- ユーザーが自分のアカウントを閉鎖できるようにする。
- パスワード確認
- ユーザーがパスワードを確認できるようにする。別のパスワード認証方法が使われている場合は多要素認証でできるようにする。
- アカウント作成
- ユーザーにアカウント作成を許可する
- ありふれたパスワードの禁止
- ありがちなパスワードの利用を禁止する。
- パスワード使い回しの禁止
- 前回と同じパスワード文字列の利用を禁止する。
- メール認証
- メール送信されたURLでログインできるようにする
- HTTP BASIC認証
- HTTP BASIC認証を利用できるようにする。
- 内部リクエスト
- メソッド呼び出しを用いてRodauthで操作を可能にする。
- JSON
- 他のすべての機能にJSON APIサポートを追加する。
- JWT CORS
- JSON APIでCORS(Cross-Origin Resource Sharing)をサポートする。
- JWTリフレッシュ
- JWTトークンへの個別アクセスとリフレッシュをサポートする。
- JWT
- 他のすべての機能にJWT(JSON Web Token)サポートを追加する。
- ロックアウト
- 無効な認証が一定回数行われたらアカウントをロックし、メール経由でロック解除できるようにする。
- ログイン
- ログイン/メールおよびパスワードでアプリケーションにログインできるようにする。
- ログアウト
- セッションからログイン情報を削除する形でアプリケーションからログアウトできるようにする。
- OTP
- TOTPによる多要素認証サポートを追加する。
- パスワードの強度
- より洗練されたパスワード強度チェックを追加する。
- パスワードの有効期限
- 指定の期間を過ぎたらパスワード変更をユーザーに要求する。
- パスワード入力の猶予期間
- ユーザーが前回パスワード入力を要求してから、フォームでのパスワード入力をスキップする猶予期間(grace period)を設定する。
- パスワード"pepper"
- 秘密キーをパスワードに追加してからパスワードをハッシュ化する。
- パスのクラスメソッド
- パスやURLをクラスメソッドで取得できるようにする。
- リカバリーコード
- 1回だけ利用できるアカウントリカバリーコードによる多要素認証サポートを追加する。
- パスワード保存
- cookieに保存されたトークンを用いて自動ログインする(いわゆるremember me)。
- パスワードのリセット
- パスワードを忘れたときにパスワードをリセットできるようにする。
- パスワードリセットの通知
- 「パスワードのリセット」機能でパスワードのリセットに成功した場合にユーザーに通知する。
- セッションの有効期限
- 一定期間操作がなかった場合やセッションのmax lifetimeを過ぎた場合にセッションを失効させる。
- シングルセッション
- 1ユーザーにつきアクティブなセッションを1つだけ許可する。
- SMSコード
- SMS(ショートメール)経由で受け取ったコードを用いる多要素認証サポートを追加する。
- パスワードハッシュの更新
- ハッシュのcostが変更された場合、常にパスワードハッシュを更新する。
- アカウントの検証
- 新規作成されたアカウントをログイン前に検証することを要求する。
- アカウント検証の猶予期間
- 新規作成されたアカウントで、アカウントの検証を要求するまでの猶予期間(grace period)を与える。
- ログイン変更の検証
- ログインを変更する前に、新たにログインしてアカウントを検証することを要求する。
- WebAuthn
- WebAuthn経由の多要素認証サポートを追加する。
- WebAuthn Autofill
- WebAuthn経由でcredentialをブラウザで自動入力するUI(別名: 条件付き調停 conditional mediation)を有効にし、ユーザーが選択するとログインする(
webauthn_login
の機能に依存)。 - WebAuthnログイン
- WebAuthn経由のパスワードレスログインのサポートを追加する。
- WebAuthnアカウント検証
- アカウント検証中のパスワードレスWebAuthnセットアップをサポートする。
🔗 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要素認証ページにリダイレクトします。require_account
require_authentication
と似ていますが、ログインしているアカウントをデータベースから読み込むことでログインしているアカウントがデータベースに存在することを確認します。アカウントがデータベースに存在するが検証されていない場合はセッションをクリアし、リクエストをログインページにリダイレクトします。logged_in?
- セッションがログイン中であるかどうかを返します。
authenticated?
logged_in?
と似ていますが、アカウントが2要素認証用に設定されている場合はセッションが2要素認証されているかどうかを返します。account!
- 現在のアカウントレコードを返します。既に読み込み済みの場合は、セッションからアカウントを取得します(ログインしている場合)。
authenticated_by
- 現在のセッションで成功した認証方式を表す文字列の配列です(例: password、remember、webauthn)。
possible_authentication_methods
- 現在のセッションで使われる可能性のある認証方式を表す文字列の配列です。
autologin_type
- 現在のセッションがautologinで認証されている場合は、使われているautologinの種別を表します。
require_two_factor_setup
- (2要素認証用)セッションで2要素認証を必須にします。2要素認証されていない場合は、リクエストを2要素認証ページにリダイレクトします。
uses_two_factor_authentication?
- (2要素認証用)現在のセッションのユーザーが2要素認証を使えるよう設定されているかどうかを返します。
update_last_activity
- (アカウント有効期限用)現在のアカウントの最終活動時刻を更新します。最終活動時刻を基にアカウントの有効期限が切れるようにしてある場合にのみ意味があります。
require_current_password
- (アカウント有効期限用)アカウントのパスワードの有効期限が切れた場合に、パスワード変更ページにリダイレクトして現在のパスワードを入力しないと継続できないようにします。
require_password_authentication
- (パスワード確認機能用)パスワードによる認証が行われておらず、かつアカウントにパスワードがある場合は、パスワード確認ページにリダイレクトし、リダイレクト前のページを保存してパスワード確認成功後に元のページにリダイレクトで戻ります。
password_grace_period
機能が使われている場合は、パスワードを最近入力していなかったときにもリダイレクトします。 load_memory
- (パスワード保存機能用)セッションが認証されていない場合に、remember cookieがあるかどうかをチェックします。有効なremember cookieがある場合はセッションに自動ログインしますが、rememberキー経由でログインしたというマークを付けます。
logged_in_via_remember_key?
- (パスワード保存機能用) rememberキーを使って現在のセッションにログインしたかどうかを返します。セキュリティ上重要な操作でパスワードの再入力を必須にしたい場合は、
confirm_password
を使えます。 http_basic_auth
- (HTTP BASIC認証機能用)指定された場合は、HTTP BASIC認証をログインに使います。
require_http_basic_auth
- (HTTP BASIC認証機能用)ログインでHTTP BASIC認証を必須にします。
check_session_expiration
- (セッション有効期限用) 現在のセッションの有効期限が切れているかどうかをチェックし、期限切れの場合は自動的にログアウトします。
check_active_session
- (アクティブセッションの機能) 現在のセッションがまだアクティブかどうかをチェックし、アクティブでない場合はセッションからログアウトします。
check_single_session
- (シングルセッションの機能)現在のセッションだけが有効かどうかをチェックし、それ以外の場合はセッションからログアウトします。
verified_account?
- (検証の猶予期間機能) 現在のアカウントが検証済みかどうかを返します。
false
の場合、猶予期間中にユーザーがログインできたことを示します。 locked_out?
- (ロックアウト機能) 現在のセッションのユーザーがロックアウトされているかどうかを返します。
authenticated_webauthn_id
- (WebAuthn機能)現在のセッションがWebAuthnで認証された場合は、使われているcredentialのWebAuthn idを返します。
*_path
- これらの1つは、Rodauthによって追加されるルーティングごとに追加され、そのルーティングへの相対パスを指定します。このメソッドに渡されるどのオプションもクエリパラメータに変換されます。
*_url
- これらの1つは、Rodauthによって追加されるルーティングごとに追加され、そのルーティングへのURLを指定します。このメソッドに渡されるどのオプションもクエリパラメータに変換されます。
🔗 他のアカウントでRodauthメソッドを呼び出す
場合によっては、ユーザーの代わりにRodauthと直接やりとりしたいことがあります(例: 既存ユーザーの代わりにアカウント作成やパスワード変更を行う)。Rodauthの内部リクエスト機能を使うと以下のように書けます。
plugin :rodauth do
enable :create_account, :change_password, :internal_request
end
rodauth.create_account(login: 'foo@example.com', password: '...')
rodauth.change_password(account_id: 24601, password: '...')
上のコードではRodaクラスのレベルでrodauth
メソッドが呼び出され、適切なRodauth::Auth
サブクラスが返されます。ユーザーの代わりにアクションを実行するには、このクラスの内部リクエストメソッドを呼び出します。詳しくは内部リクエストのドキュメントを参照してください。
🔗 Rodauthをライブラリとして利用する
RodauthはRackアプリケーション向けの認証フレームワークとして機能するように設計されていますが、Webアプリケーションの外部で純粋なライブラリとして利用することも可能です。
これを行うには、以下のようにrodauth
をrequire
してからRodauth.lib
メソッドでRodauth::Auth
サブクラスを返し、そのサブクラスでメソッドを呼び出します。Rodauth.lib
メソッドにはRodauthプラグインのオプションハッシュとRodauth設定用のブロックを渡します。
require 'rodauth'
rodauth = Rodauth.lib do
enable :create_account, :change_password
end
rodauth.create_account(login: 'foo@example.com', password: '...')
rodauth.change_password(account_id: 24601, password: '...')
これは内部リクエストサポートの上に構築されており(設定ブロックを処理する前に内部リクエスト機能を暗黙で読み込みます)、非WebアプリケーションでRodauthを利用できます。ただし、Rodauthのデータストレージ用にSequel::Database
コネクションを引き続きセットアップする必要もあります。
🔗 複数の設定を使い分ける
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
デフォルトでは、セカンダリ設定でも同じセッションキーがプライマリ設定として使われますが、これが望ましくない場合もあります。設定ごとにセッションのステートを確実に分離するために、別の設定セッションキーのプレフィックスをそれぞれ設定できます。両方の設定でパスワード保存(remember me)機能を使うときに、以下のようにセカンダリ設定で異なるrememberキーを設定することも可能です。
plugin :rodauth, name: :secondary do
session_key_prefix "secondary_"
remember_cookie_key "_secondary_remember"
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[:email], 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
# パスワードがない!
password_match? do |pass|
true
end
end
🔗 Railsで利用する
Railsを使っている場合は、RodauthをRailsに統合するrodauth-rails gemを利用できます。このgemには以下の機能も含まれています。
- ビューやメイラーのジェネレータに加え、RodauthやSequel設定ファイル向けジェネレータも利用できる
-
RailsのFlashメッセージやCSRF保護を利用できる
-
HMAC secretを自動的にRailsのsecretキーベースに設定する
-
テンプレートのレンダリングにAction ControllerとAction Viewを利用する
-
メール送信にAction Mailerを利用する
詳しくはrodauth-railsのREADMEを参照してください。
🔗 その他の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メソッドを簡単に呼び出せる方法をアプリに導入できるようになります。
パスワード保存機能(remember me)機能でextend_remember_deadline?
設定をtrueにする場合は、Rodaのミドルウェアプラグインをforward_response_headers: true
オプション付きで読み込むことで、リクエストがメインアプリに転送されたときにroute
ブロック内のload_memory
呼び出しによるSet-Cookieヘッダーの変更を伝搬させることも可能です。
Rodaを使わないアプリでのRodauth導入例をいくつか示します。
🔗 2要素認証を利用する
Rodauthには以下の手法を経由する2要素認証が同梱されています。
- WebAuthn
- TOTP(Time-Based One-Time Passwords、RFC 6238)
- SMSコード
- リカバリーコード
アプリケーションの用途に応じてRodauthと2要素認証を統合する方法はいくつもあります。SMSコードとリカバリーコードは、デフォルトでは2要素認証のバックアップとしてのみ扱われるので、これらを有効にするには別の2要素認証を有効にしなければなりません。ただし、これは設定で変更できます。
アプリで2要素認証をサポートし、かつ2要素認証を必須にしたくない場合は次のようにします。
plugin :rodauth do
enable :login, :logout, :otp, :recovery_codes, :sms_codes
end
route do |r|
r.rodauth
rodauth.require_authentication
# ...
end
以下は、2要素認証を全ユーザーで必須にし、アカウントを持っていないユーザーに対して2要素認証の設定を要求する場合の設定です。
route do |r|
r.rodauth
rodauth.require_authentication
rodauth.require_two_factor_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を構築する目的で、Rodauthで通常読み込まれるHTML関連のプラグイン(render、csrf、flash、h)を読み込まないようにしたい場合は、以下のように:json => :only
を渡します。
plugin :rodauth, :json=>:only do
enable :login, :logout, :jwt
end
ただし、メール送信機能はデフォルトでrenderプラグインに依存している点にご注意ください。json: :only
を使う場合は、renderプラグインを手動で読み込むか、*_email_body
設定オプションでメールの本文を指定する必要があります。
JWT機能を導入すると、Rodauthに含まれるその他のJSON APIサポートもすべて利用できるようになります。セッションデータの保存先にRackセッションを用いるJSON APIを使いたい場合は、代わりに以下のようにJSON機能を有効にします。
plugin :rodauth, json: true do
enable :login, :logout, :json
only_json? true # JSONリクエストのみを扱いたい場合
end
🔗 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
🔗 外部の機能を使う
設定のenable
メソッドは、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
機能の構成方法の完全な例については、internalガイドを参照してください。
🔗 ルーティングレベルの挙動をオーバーライドする
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
🔗 Rubyサポートポリシー
Rodauthは、現在サポートされているバージョンのRuby(MRI)およびJRubyを完全にサポートします。サポートされていないバージョンのRubyやJRubyをサポートする可能性もありますが、サポート上問題になる場合はマイナーバージョンでサポートが終了する可能性もあります。現在のRodauthが動作するために必要なRubyの最小バージョンは1.9.2です。
🔗 類似のプロジェクト
以下はすべてRailsに特化しています。
🔗 著者
Jeremy Evans (code@jeremyevans.net)
概要
MITライセンスに基いて翻訳・公開します。
RodauthのREADMEは認証のセキュリティを考えるうえで参考になる情報が多く、ドキュメントの質が非常に高いのが特徴です。大きな概要をまず示し、必要な概念も適宜示しながら、先に進むに連れて詳細に説明するという書き方が見事です。
さらに、READMEから読み取れる筋のよい認証システム設計も参考になると思います。パスワードハッシュの保存場所を完全に隔離した上で可能な限り自由度を高めている点や、機能ごとにカラムを足したり減らしたりするのではなくテーブルを足したり減らしたりする点など、学ぶところが多そうです。Rodauthは継続的にメンテナンスされているのも頼もしい点です。