Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: 認証gem「Rodauth」README(更新翻訳)

概要

MITライセンスに基いて翻訳・公開します。

jeremyevans/rodauth - GitHub

RodauthのREADMEは認証のセキュリティを考えるうえで参考になる情報が多く、ドキュメントの質が非常に高いのが特徴です。大きな概要をまず示し、必要な概念も適宜示しながら、先に進むに連れて詳細に説明するという書き方が見事です。

さらに、READMEから読み取れる筋のよい認証システム設計も参考になると思います。パスワードハッシュの保存場所を完全に隔離した上で可能な限り自由度を高めている点や、機能ごとにカラムを足したり減らしたりするのではなくテーブルを足したり減らしたりする点など、学ぶところが多そうです。Rodauthは継続的にメンテナンスされているのも頼もしい点です。

更新履歴

  • 2017/10/17: 初版公開
  • 2022/11/11: バージョン2.26.1のREADMEに沿って更新
  • 2023/07/27: バージョン2.30.0のREADMEに沿って更新

更新前のREADME翻訳は以下です。

Ruby: 認証gem「Rodauth」README(翻訳)

参考

このRodauthを元にjankoさんが精力的に開発しているrodauth-rails gem↓と関連記事もチェックしてみてください。

janko/rodauth-rails - GitHub

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で利用
mail
メールのreset_passwordverify_accountverify_login_changechange_password_notifylockoutemail_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のセキュリティ設計をすべて利用するには、複数のデータベースアカウントを使います。

  1. データベースのsuperuserアカウント(通常はpostgres)
  2. appアカウント(実際はアプリと同じ名前にします)
  3. 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拡張を読み込む必要はありません。その場合は、マイグレーション内のcitextStringに変更し、メールアドレスに対応できるようにしてください。

🔗 スキーマ権限の一時的な付与(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 OPTIONphアカウントから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. 認証系メソッド

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アプリケーションの外部で純粋なライブラリとして利用することも可能です。

これを行うには、以下のようにrodauthrequireしてから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で利用する

janko/rodauth-rails - GitHub

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に特化しています。

heartcombo/devise - GitHub

binarylogic/authlogic - GitHub

Sorcery/sorcery - GitHub

🔗 著者

Jeremy Evans (code@jeremyevans.net

🔗 関連記事

Rails: 認証gem ‘rodauth-rails’ README(翻訳)

Rails: 認証gem ‘Rodauth’を統合するrodauth-railsを開発しました(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。