{"id":13411619,"url":"https://github.com/jeremyevans/rodauth","last_synced_at":"2025-05-13T15:10:19.017Z","repository":{"id":36302910,"uuid":"40607511","full_name":"jeremyevans/rodauth","owner":"jeremyevans","description":"Ruby's Most Advanced Authentication Framework","archived":false,"fork":false,"pushed_at":"2025-05-09T20:23:07.000Z","size":5106,"stargazers_count":1782,"open_issues_count":0,"forks_count":100,"subscribers_count":27,"default_branch":"master","last_synced_at":"2025-05-09T20:31:43.156Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://rodauth.jeremyevans.net","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jeremyevans.png","metadata":{"files":{"readme":"README.rdoc","changelog":"CHANGELOG","contributing":"CONTRIBUTING","funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2015-08-12T15:11:41.000Z","updated_at":"2025-05-09T20:23:11.000Z","dependencies_parsed_at":"2023-02-13T23:46:46.379Z","dependency_job_id":"542d34f9-597d-4480-af59-73d65c9b623f","html_url":"https://github.com/jeremyevans/rodauth","commit_stats":{"total_commits":1222,"total_committers":35,"mean_commits":34.91428571428571,"dds":"0.15548281505728312","last_synced_commit":"cb72e987eadd65d6d90418aa9708141071aa59c1"},"previous_names":[],"tags_count":68,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyevans%2Frodauth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyevans%2Frodauth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyevans%2Frodauth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremyevans%2Frodauth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeremyevans","download_url":"https://codeload.github.com/jeremyevans/rodauth/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253324226,"owners_count":21890847,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T20:01:15.063Z","updated_at":"2025-05-13T15:10:13.993Z","avatar_url":"https://github.com/jeremyevans.png","language":"Ruby","funding_links":[],"categories":["Ruby","Authentication and OAuth"],"sub_categories":[],"readme":"= Rodauth\n\nRodauth is Ruby's most advanced authentication framework, designed\nto work in any rack application.  It's built using Roda and Sequel,\nbut it can be used with other web frameworks, database libraries,\nand databases.\n\nWhen used with PostgreSQL, MySQL, and Microsoft SQL Server in the\ndefault configuration, it offers additional security for password\nhashes by protecting access via database functions.\n\nRodauth supports multiple multifactor authentication methods,\nmultiple passwordless authentication methods, and offers both an\nHTML and JSON API for all supported features.\n\n== Design Goals\n\n* Security: Ship in a maximum security by default configuration\n* Simplicity: Allow for easy configuration via a DSL\n* Flexibility: Allow for easy overriding of any part of the framework\n\n== Features\n\n* Login\n* Logout\n* Change Password\n* Change Login\n* Reset Password\n* Create Account\n* Close Account\n* Verify Account\n* Confirm Password\n* Remember (Autologin via token)\n* Lockout (Bruteforce protection)\n* Audit Logging\n* Email Authentication (Passwordless login via email link)\n* WebAuthn (Multifactor authentication via WebAuthn)\n* WebAuthn Login (Passwordless login via WebAuthn)\n* WebAuthn Verify Account (Passwordless WebAuthn Setup)\n* WebAuthn Autofill (Autofill WebAuthn credentials on login)\n* WebAuthn Modify Email (Email when WebAuthn authenticator added or removed)\n* OTP (Multifactor authentication via TOTP)\n* OTP Modify Email (Email when TOTP authentication setup or disabled)\n* OTP Unlock (Unlock TOTP authentication after lockout)\n* OTP Lockout Email (Email when TOTP authentication locked out or unlocked)\n* Recovery Codes (Multifactor authentication via backup codes)\n* SMS Codes (Multifactor authentication via SMS)\n* Verify Login Change (Verify new login before changing login)\n* Verify Account Grace Period (Don't require verification before login)\n* Password Grace Period (Don't require password entry if recently entered)\n* Password Complexity (More sophisticated checks)\n* Password Pepper\n* Disallow Password Reuse\n* Disallow Common Passwords\n* Password Expiration\n* Account Expiration\n* Session Expiration\n* Active Sessions (Prevent session reuse after logout, allow logout of all sessions)\n* Single Session (Only one active session per account)\n* JSON (JSON API support for all other features)\n* JWT (JSON Web Token support for all other features)\n* JWT Refresh (Access \u0026 Refresh Token)\n* JWT CORS (Cross-Origin Resource Sharing)\n* Update Password Hash (when hash cost changes)\n* Argon2\n* HTTP Basic Auth\n* Change Password Notify\n* Reset Password Notify\n* Internal Request\n* Path Class Methods\n\n== Resources\n\nWebsite :: http://rodauth.jeremyevans.net\nDemo Site :: http://rodauth-demo.jeremyevans.net\nSource :: http://github.com/jeremyevans/rodauth\nBugs :: http://github.com/jeremyevans/rodauth/issues\nDiscussion Forum (GitHub Discussions) :: https://github.com/jeremyevans/rodauth/discussions\nAlternate Discussion Forum (Google Groups) :: https://groups.google.com/forum/#!forum/rodauth\n\n== Dependencies\n\nThere are some dependencies that Rodauth uses depending on the\nfeatures in use. These are development dependencies instead of\nruntime dependencies in the gem as it is possible to run without them:\n\ntilt :: Used by all features unless in JSON API only mode or using\n        :render=\u003efalse plugin option.\nrack_csrf :: Used for CSRF support if the \u003ctt\u003ecsrf: :rack_csrf\u003c/tt\u003e plugin\n             option is given (the default is to use Roda's route_csrf\n             plugin, as that allows for more secure request-specific\n             tokens).\nbcrypt :: Used by default for password hashing, can be skipped\n          if password_match? is overridden for custom authentication.\nargon2 :: Used by the argon2 feature as alternative to bcrypt for\n          password hashing.\nmail :: Used by default for mailing in the reset_password, verify_account,\n        verify_login_change, change_password_notify, lockout, and\n        email_auth features.\nrotp :: Used by the otp feature\nrqrcode :: Used by the otp feature\njwt :: Used by the jwt feature\nwebauthn :: Used by the webauthn feature\n\nYou can use \u003ctt\u003egem install --development rodauth\u003c/tt\u003e to install\nthe development dependencies in order to run tests.\n\n== Security\n\n=== Password Hash Access Via Database Functions\n\nBy default on PostgreSQL, MySQL, and Microsoft SQL Server, Rodauth\nuses database functions to access password hashes, with the user\nrunning the application unable to get direct access to password\nhashes.  This reduces the risk of an attacker being able to access\npassword hashes and use them to attack other sites.\n\nThe rest of this section describes this feature in more detail, but\nnote that Rodauth does not require this feature be used and works\ncorrectly without it.  There may be cases where you cannot use\nthis feature, such as when using a different database or when you\ndo not have full control over the database you are using.\n\nPasswords are hashed using bcrypt by default, and the password hashes are\nkept in a separate table from the accounts table, with a foreign key\nreferencing the accounts table.  Two database functions are added,\none to retrieve the salt for a password, and the other to check\nif a given password hash matches the password hash for the user.\n\nTwo database accounts are used. The first is the account that the\napplication uses, which is referred to as the +app+ account. The +app+\naccount does not have access to read the password hashes. The other\naccount handles password hashes and is referred to as the +ph+\naccount.  The +ph+ account sets up the database functions that can\nretrieve the salt for a given account's password, and check if a\npassword hash matches for a given account.  The +ph+ account\nsets these functions up so that the +app+ account can execute the\nfunctions using the +ph+ account's permissions.  This allows the\n+app+ account to check passwords without having access to read\npassword hashes.\n\nWhile the +app+ account is not be able to read password hashes, it\nis still be able to insert password hashes, update passwords hashes,\nand delete password hashes, so the additional security is not that\npainful.\n\nBy disallowing the +app+ account access to the password hashes,\nit is much more difficult for an attacker to access the password\nhashes, even if they are able to exploit an SQL injection or remote\ncode execution vulnerability in the application.\n\nThe reason for extra security in regards to password hashes stems from\nthe fact that people tend to choose poor passwords and reuse passwords,\nso a compromise of one database containing password hashes can result\nin account access on other sites, making password hash storage of\ncritical importance even if the other data stored is not that important.\n\nIf you are storing other sensitive information in your database, you\nshould consider using a similar approach in other areas (or all areas)\nof your application.\n\n=== Tokens\n\nAccount verification, password resets, email auth, verify login change,\nremember, and lockout tokens all use a similar approach.  They all\nprovide a token, in the format \"account-id_long-random-string\".  By\nincluding the id of the account in the token, an attacker can only\nattempt to bruteforce the token for a single account, instead of being\nable to bruteforce tokens for all accounts at once (which would be\npossible if the token was just a random string).\n\nAdditionally, all comparisons of tokens use a timing-safe comparison\nfunction to reduce the risk of timing attacks.\n\n== HMAC\n\nBy default, for backwards compatibility, Rodauth does not use HMACs,\nbut you are strongly encouraged to use the +hmac_secret+ configuration\nmethod to set an HMAC secret.  Setting an HMAC secret will enable HMACs\nfor additional security, as described below.\n\n=== email_base feature\n\nAll features that send email use this feature.  Setting +hmac_secret+\nwill make the tokens sent via email use an HMAC, while the raw token\nstored in the database will not use an HMAC.  This will make it so\nif the tokens in the database are leaked (e.g. via an SQL injection\nvulnerability), they will not be usable without also having access\nto the +hmac_secret+.  Without an HMAC, the raw token is sent in the\nemail, and if the tokens in the database are leaked, they will be\nusable.\n\nTo allow for an graceful transition, you can set +allow_raw_email_token?+\nto true temporarily.  This will allow the raw tokens in previous sent\nemails to still work.  This should only be set temporarily as it\nremoves the security that +hmac_secret+ adds.  Most features that\nsend email have tokens that expire by default in 1 day.  The\nexception is the verify_account feature, which has tokens that do\nnot expire.  For the verify_account feature, if the user requested\nan email before +hmac_secret+ was set, after +allow_raw_email_token+\nis no longer set, they will need to request the verification email\nbe resent, in which case they will receive an email with a token\nthat uses an HMAC.\n\n=== remember feature\n\nSimilar to the email_base feature, this uses HMACs for remember\ntokens, while storing the raw tokens in the database.  This makes\nit so if the raw tokens in the database are leaked, the remember\ntokens are not usable without knowledge of the +hmac_secret+.\n\nThe +raw_remember_token_deadline+ configuration method can\nbe set to allow a previously set raw remember token to be used\nif the deadline for the remember token is before the given time.\nThis allows for graceful transition to using HMACs for remember tokens.\nBy default, the deadline is 14 days after the token is created, so this\nshould be set to 14 days after the time you enable the HMAC for the\nremember feature if you are using the defaults.\n\n=== otp feature\n\nSetting +hmac_secret+ will provide HMACed OTP keys to users, and\nwould store the raw OTP keys in the database.  This will make so\nif the raw OTP keys in the database are leaked, they will not be\nusable for two factor authentication without knowledge of the +hmac_secret+.\n\nUnfortunately, there can be no simple graceful transition for existing users.\nWhen introducing +hmac_secret+ to a Rodauth installation that already uses\nthe otp feature, you will have to either revoke and replace all OTP keys,\nset +otp_keys_use_hmac?+ to false and continue to use raw OTP keys, or override\n+otp_keys_use_hmac?+ to return false if the user was issued an OTP key before\n+hmac_secret+ was added to the configuration, and true otherwise.\n+otp_keys_use_hmac?+ defaults to true if +hmac_secret+ is set, and false\notherwise.\n\nIf +otp_keys_use_hmac?+ is true, Rodauth will also ensure during OTP setup\nthat the OTP key was generated by the server.  If +otp_keys_use_hmac?+ is false,\nany OTP key in a valid format will be accepted during setup.\n\nIf +otp_keys_use_hmac?+ is true, the jwt and otp features are in use and you\nare setting up OTP via JSON requests, you need to first send a POST request\nto the OTP setup route.  This will return an error with the +otp_secret+ and\n+otp_raw_secret+ parameters in the JSON.  These parameters should be submitted\nin the POST request to setup OTP, along with a valid OTP auth code for the\n+otp_secret+.\n\n=== webauthn feature\n\nSetting +hmac_secret+ is required to use the webauthn feature, as it is\nused for checking that the provided authentication challenges have not\nbeen modified.\n\n=== active_sessions feature\n\nSetting +hmac_secret+ is required to use the active_sessions feature,\nas the database stores an HMAC of the active session ID.\n\n=== single_session feature\n\nSetting +hmac_secret+ will ensure the single session secret set in the\nsession will be an HMACed.  This does not affect security, as the session\nitself should at the least by protected by an HMAC (if not encrypted).\nThis is only done for consistency, so that the raw tokens in the database\nare distinct from the tokens provided to the users.  To allow for a\ngraceful transition, +allow_raw_single_session_key?+ can be set to true.\n\n== PostgreSQL Database Setup\n\nIn order to get full advantages of Rodauth's security design on PostgreSQL,\nmultiple database accounts are involved:\n\n1. database superuser account (usually postgres)\n2. +app+ account (same name as application)\n3. +ph+ account (application name with +_password+ appended)\n\nThe database superuser account is used to load extensions related to the\ndatabase.  The application should never be run using the database\nsuperuser account.\n\n=== Create database accounts\n\nIf you are currently running your application using the database superuser\naccount, the first thing you need to do is to create the +app+ database\naccount.  It's often best to name this account the same as the\ndatabase name.\n\nYou should also create the +ph+ database account which will handle access\nto the password hashes.\n\nExample for PostgreSQL:\n\n  createuser -U postgres ${DATABASE_NAME}\n  createuser -U postgres ${DATABASE_NAME}_password\n\nNote that if the database superuser account owns all of the items in the\ndatabase, you'll need to change the ownership to the database account you\njust created.  See https://gist.github.com/jeremyevans/8483320\nfor a way to do that.\n\n=== Create database\n\nIn general, the +app+ account is the owner of the database, since it will\nown most of the tables:\n\n  createdb -U postgres -O ${DATABASE_NAME} ${DATABASE_NAME}\n\nNote that this is not the most secure way to develop applications.  For\nmaximum security, you would want to use a separate database account as\nthe owner of the tables, have the +app+ account not be the\nowner of any tables, and specifically grant the +app+ account only the\nminimum access it needs to work correctly.  Doing that is beyond the\nscope of Rodauth, though.\n\n=== Load extensions\n\nIf you want to use the login features for Rodauth, you need to load the\ncitext extension if you want to support case insensitive logins.\n\nExample:\n\n  psql -U postgres -c \"CREATE EXTENSION citext\" ${DATABASE_NAME}\n\nNote that on Heroku, this extension can be loaded using a standard database\naccount.  If you want logins to be case sensitive (generally considered a\nbad idea), you don't need to use the PostgreSQL citext extension. Just\nremember to modify the migration below to use +String+ instead of +citext+\nfor the email in that case.\n\n=== Grant schema rights (PostgreSQL 15+)\n\nPostgreSQL 15 changed default database security so that only the database\nowner has writable access to the public schema.  Rodauth expects the\n+ph+ account to have writable access to the public schema when setting\nthings up.  Temporarily grant that access (it will be revoked after the\nmigration has run)\n\n  psql -U postgres -c \"GRANT CREATE ON SCHEMA public TO ${DATABASE_NAME}_password\" ${DATABASE_NAME}\n\n=== Using non-default schema\n\nPostgreSQL sets up new tables in the public schema by default.\nIf you would like to use separate schemas per user, you can do:\n\n  psql -U postgres -c \"DROP SCHEMA public;\" ${DATABASE_NAME}\n  psql -U postgres -c \"CREATE SCHEMA AUTHORIZATION ${DATABASE_NAME};\"          ${DATABASE_NAME}\n  psql -U postgres -c \"CREATE SCHEMA AUTHORIZATION ${DATABASE_NAME}_password;\" ${DATABASE_NAME}\n  psql -U postgres -c \"GRANT USAGE ON SCHEMA ${DATABASE_NAME}          TO ${DATABASE_NAME}_password;\" ${DATABASE_NAME}\n  psql -U postgres -c \"GRANT USAGE ON SCHEMA ${DATABASE_NAME}_password TO ${DATABASE_NAME};\"          ${DATABASE_NAME}\n\nYou'll need to modify the code to load the extension to specify the schema:\n\n  psql -U postgres -c \"CREATE EXTENSION citext SCHEMA ${DATABASE_NAME}\" ${DATABASE_NAME}\n\nWhen running the migration for the +ph+ user you'll need to modify a couple\nthings for the schema changes:\n\n  create_table(:account_password_hashes) do\n    foreign_key :id, Sequel[:${DATABASE_NAME}][:accounts], primary_key: true, type: :Bignum\n    String :password_hash, null: false\n  end\n  Rodauth.create_database_authentication_functions(self, table_name: Sequel[:${DATABASE_NAME}_password][:account_password_hashes])\n\n  # if using the disallow_password_reuse feature:\n  create_table(:account_previous_password_hashes) do\n    primary_key :id, type: :Bignum\n    foreign_key :account_id, Sequel[:${DATABASE_NAME}][:accounts], type: :Bignum\n    String :password_hash, null: false\n  end\n  Rodauth.create_database_previous_password_check_functions(self, table_name: Sequel[:${DATABASE_NAME}_password][:account_previous_password_hashes])\n\nYou'll also need to use the following Rodauth configuration methods so that the\napp account calls functions in a separate schema:\n\n  function_name do |name|\n    \"${DATABASE_NAME}_password.#{name}\"\n  end\n  password_hash_table Sequel[:${DATABASE_NAME}_password][:account_password_hashes]\n\n  # if using the disallow_password_reuse feature:\n  previous_password_hash_table Sequel[:${DATABASE_NAME}_password][:account_previous_password_hashes]\n\n== MySQL Database Setup\n\nMySQL does not have the concept of object owners, and MySQL's GRANT/REVOKE\nsupport is much more limited than PostgreSQL's. When using MySQL, it is\nrecommended to GRANT the +ph+ account ALL privileges on the database,\nincluding the ability to GRANT permissions to the +app+ account:\n\n  CREATE USER '${DATABASE_NAME}'@'localhost' IDENTIFIED BY '${PASSWORD}';\n  CREATE USER '${DATABASE_NAME}_password'@'localhost' IDENTIFIED BY '${OTHER_PASSWORD}';\n  GRANT ALL ON ${DATABASE_NAME}.* TO '${DATABASE_NAME}_password'@'localhost' WITH GRANT OPTION;\n\nYou should run all migrations as the +ph+ account, and GRANT specific access\nto the +app+ account as needed.\n\nAdding the database functions on MySQL may require setting the \n\u003ctt\u003elog_bin_trust_function_creators=1\u003c/tt\u003e setting in the MySQL configuration.\n\n== Microsoft SQL Server Database Setup\n\nMicrosoft SQL Server has a concept of database owners, but similar to MySQL\nusage it's recommended to use the +ph+ account as the superuser for the\ndatabase, and have it GRANT permissions to the +app+ account:\n\n  CREATE LOGIN rodauth_test WITH PASSWORD = 'rodauth_test';\n  CREATE LOGIN rodauth_test_password WITH PASSWORD = 'rodauth_test';\n  CREATE DATABASE rodauth_test;\n  USE rodauth_test;\n  CREATE USER rodauth_test FOR LOGIN rodauth_test;\n  GRANT CONNECT, EXECUTE TO rodauth_test;\n  EXECUTE sp_changedbowner 'rodauth_test_password';\n\nYou should run all migrations as the +ph+ account, and GRANT specific access\nto the +app+ account as needed.\n\n== Creating tables\n\nBecause two different database accounts are used, two different migrations\nare required, one for each database account.  Here are example migrations.\nYou can modify them to add support for additional columns, or remove tables\nor columns related to features that you don't need.\n\nFirst migration.  On PostgreSQL, this should be run with the +app+ account,\non MySQL and Microsoft SQL Server this should be run with the +ph+ account.\n\nNote that these migrations require Sequel 4.35.0+.\n\n  Sequel.migration do\n    up do\n      extension :date_arithmetic\n\n      # Used by the account verification and close account features\n      create_table(:account_statuses) do\n        Integer :id, primary_key: true\n        String :name, null: false, unique: true\n      end\n      from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])\n\n      db = self\n      create_table(:accounts) do\n        primary_key :id, type: :Bignum\n        foreign_key :status_id, :account_statuses, null: false, default: 1\n        if db.database_type == :postgres\n          citext :email, null: false\n          constraint :valid_email, email: /^[^,;@ \\r\\n]+@[^,@; \\r\\n]+\\.[^,@; \\r\\n]+$/\n        else\n          String :email, null: false\n        end\n        if db.supports_partial_indexes?\n          index :email, unique: true, where: {status_id: [1, 2]}\n        else\n          index :email, unique: true\n        end\n      end\n\n      deadline_opts = proc do |days|\n        if database_type == :mysql\n          {null: false}\n        else\n          {null: false, default: Sequel.date_add(Sequel::CURRENT_TIMESTAMP, days: days)}\n        end\n      end\n\n      # Used by the audit logging feature\n      json_type = case database_type\n      when :postgres\n        :jsonb\n      when :sqlite, :mysql\n        :json\n      else\n        String\n      end\n      create_table(:account_authentication_audit_logs) do\n        primary_key :id, type: :Bignum\n        foreign_key :account_id, :accounts, null: false, type: :Bignum\n        DateTime :at, null: false, default: Sequel::CURRENT_TIMESTAMP\n        String :message, null: false\n        column :metadata, json_type\n        index [:account_id, :at], name: :audit_account_at_idx\n        index :at, name: :audit_at_idx\n      end\n\n      # Used by the password reset feature\n      create_table(:account_password_reset_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        DateTime :deadline, deadline_opts[1]\n        DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      # Used by the jwt refresh feature\n      create_table(:account_jwt_refresh_keys) do\n        primary_key :id, type: :Bignum\n        foreign_key :account_id, :accounts, null: false, type: :Bignum\n        String :key, null: false\n        DateTime :deadline, deadline_opts[1]\n        index :account_id, name: :account_jwt_rk_account_id_idx\n      end\n\n      # Used by the account verification feature\n      create_table(:account_verification_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        DateTime :requested_at, null: false, default: Sequel::CURRENT_TIMESTAMP\n        DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      # Used by the verify login change feature\n      create_table(:account_login_change_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        String :login, null: false\n        DateTime :deadline, deadline_opts[1]\n      end\n\n      # Used by the remember me feature\n      create_table(:account_remember_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        DateTime :deadline, deadline_opts[14]\n      end\n\n      # Used by the lockout feature\n      create_table(:account_login_failures) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        Integer :number, null: false, default: 1\n      end\n      create_table(:account_lockouts) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        DateTime :deadline, deadline_opts[1]\n        DateTime :email_last_sent\n      end\n\n      # Used by the email auth feature\n      create_table(:account_email_auth_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        DateTime :deadline, deadline_opts[1]\n        DateTime :email_last_sent, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      # Used by the password expiration feature\n      create_table(:account_password_change_times) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        DateTime :changed_at, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      # Used by the account expiration feature\n      create_table(:account_activity_times) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        DateTime :last_activity_at, null: false\n        DateTime :last_login_at, null: false\n        DateTime :expired_at\n      end\n\n      # Used by the single session feature\n      create_table(:account_session_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n      end\n\n      # Used by the active sessions feature\n      create_table(:account_active_session_keys) do\n        foreign_key :account_id, :accounts, type: :Bignum\n        String :session_id\n        Time :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP\n        Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP\n        primary_key [:account_id, :session_id]\n      end\n\n      # Used by the webauthn feature\n      create_table(:account_webauthn_user_ids) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :webauthn_id, null: false\n      end\n      create_table(:account_webauthn_keys) do\n        foreign_key :account_id, :accounts, type: :Bignum\n        String :webauthn_id\n        String :public_key, null: false\n        Integer :sign_count, null: false\n        Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP\n        primary_key [:account_id, :webauthn_id]\n      end\n\n      # Used by the otp feature\n      create_table(:account_otp_keys) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :key, null: false\n        Integer :num_failures, null: false, default: 0\n        Time :last_use, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      # Used by the otp_unlock feature\n      create_table(:account_otp_unlocks) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        Integer :num_successes, null: false, default: 1\n        Time :next_auth_attempt_after, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      # Used by the recovery codes feature\n      create_table(:account_recovery_codes) do\n        foreign_key :id, :accounts, type: :Bignum\n        String :code\n        primary_key [:id, :code]\n      end\n\n      # Used by the sms codes feature\n      create_table(:account_sms_codes) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :phone_number, null: false\n        Integer :num_failures\n        String :code\n        DateTime :code_issued_at, null: false, default: Sequel::CURRENT_TIMESTAMP\n      end\n\n      case database_type\n      when :postgres\n        user = get(Sequel.lit('current_user')) + '_password'\n        run \"GRANT REFERENCES ON accounts TO #{user}\"\n      when :mysql, :mssql\n        user = if database_type == :mysql\n          get(Sequel.lit('current_user')).sub(/_password@/, '@')\n        else\n          get(Sequel.function(:DB_NAME))\n        end\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_statuses TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON accounts TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_authentication_audit_logs TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_reset_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_jwt_refresh_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_verification_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_change_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_remember_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_login_failures TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_email_auth_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_lockouts TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_password_change_times TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_activity_times TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_session_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_active_session_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_user_ids TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_webauthn_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_keys TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_otp_unlocks TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_recovery_codes TO #{user}\"\n        run \"GRANT SELECT, INSERT, UPDATE, DELETE ON account_sms_codes TO #{user}\"\n      end\n    end\n\n    down do\n      drop_table(:account_sms_codes,\n                 :account_recovery_codes,\n                 :account_otp_unlocks,\n                 :account_otp_keys,\n                 :account_webauthn_keys,\n                 :account_webauthn_user_ids,\n                 :account_session_keys,\n                 :account_active_session_keys,\n                 :account_activity_times,\n                 :account_password_change_times,\n                 :account_email_auth_keys,\n                 :account_lockouts,\n                 :account_login_failures,\n                 :account_remember_keys,\n                 :account_login_change_keys,\n                 :account_verification_keys,\n                 :account_jwt_refresh_keys,\n                 :account_password_reset_keys,\n                 :account_authentication_audit_logs,\n                 :accounts,\n                 :account_statuses)\n    end\n  end\n\nSecond migration, run using the +ph+ account:\n\n  require 'rodauth/migrations'\n\n  Sequel.migration do\n    up do\n      create_table(:account_password_hashes) do\n        foreign_key :id, :accounts, primary_key: true, type: :Bignum\n        String :password_hash, null: false\n      end\n      Rodauth.create_database_authentication_functions(self)\n      case database_type\n      when :postgres\n        user = get(Sequel.lit('current_user')).sub(/_password\\z/, '')\n        run \"REVOKE ALL ON account_password_hashes FROM public\"\n        run \"REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public\"\n        run \"REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public\"\n        run \"GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}\"\n        run \"GRANT SELECT(id) ON account_password_hashes TO #{user}\"\n        run \"GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}\"\n        run \"GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}\"\n      when :mysql\n        user = get(Sequel.lit('current_user')).sub(/_password@/, '@')\n        db_name = get(Sequel.function(:database))\n        run \"GRANT EXECUTE ON #{db_name}.* TO #{user}\"\n        run \"GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}\"\n        run \"GRANT SELECT (id) ON account_password_hashes TO #{user}\"\n      when :mssql\n        user = get(Sequel.function(:DB_NAME))\n        run \"GRANT EXECUTE ON rodauth_get_salt TO #{user}\"\n        run \"GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}\"\n        run \"GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}\"\n        run \"GRANT SELECT ON account_password_hashes(id) TO #{user}\"\n      end\n\n      # Used by the disallow_password_reuse feature\n      create_table(:account_previous_password_hashes) do\n        primary_key :id, type: :Bignum\n        foreign_key :account_id, :accounts, type: :Bignum\n        String :password_hash, null: false\n      end\n      Rodauth.create_database_previous_password_check_functions(self)\n\n      case database_type\n      when :postgres\n        user = get(Sequel.lit('current_user')).sub(/_password\\z/, '')\n        run \"REVOKE ALL ON account_previous_password_hashes FROM public\"\n        run \"REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public\"\n        run \"REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public\"\n        run \"GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}\"\n        run \"GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}\"\n        run \"GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}\"\n        run \"GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}\"\n        run \"GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}\"\n      when :mysql\n        user = get(Sequel.lit('current_user')).sub(/_password@/, '@')\n        db_name = get(Sequel.function(:database))\n        run \"GRANT EXECUTE ON #{db_name}.* TO #{user}\"\n        run \"GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}\"\n        run \"GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}\"\n      when :mssql\n        user = get(Sequel.function(:DB_NAME))\n        run \"GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}\"\n        run \"GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}\"\n        run \"GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}\"\n        run \"GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}\"\n      end\n    end\n\n    down do\n      Rodauth.drop_database_previous_password_check_functions(self)\n      Rodauth.drop_database_authentication_functions(self)\n      drop_table(:account_previous_password_hashes, :account_password_hashes)\n    end\n  end\n\nTo support multiple separate migration users, you can run the migration\nfor the password user using Sequel's migration API: \n\n  Sequel.extension :migration\n  Sequel.postgres('DATABASE_NAME', user: 'PASSWORD_USER_NAME') do |db|\n    Sequel::Migrator.run(db, 'path/to/password_user/migrations', table: 'schema_info_password')\n  end\n\nIf the database is not PostgreSQL, MySQL, or Microsoft SQL Server, or you\ncannot use multiple user accounts, just combine the two migrations into a\nsingle migration, removing all the code related to database permissions\nand database functions.\n\nOne thing to notice in the above migrations is that Rodauth uses additional\ntables for additional features, instead of additional columns in a single\ntable.\n\n=== Revoking schema rights (PostgreSQL 15+)\n\nIf you explicit granted access to the public schema before running the\nmigration, revoke it afterward:\n\n  psql -U postgres -c \"REVOKE CREATE ON SCHEMA public FROM ${DATABASE_NAME}_password\" ${DATABASE_NAME}\n\n=== Locking Down (PostgreSQL only)\n\nAfter running the migrations, you can increase security slightly by making\nit not possible for the +ph+ account to login to the database directly.\nThis can be accomplished by modifying the +pg_hba.conf+ file.  You can also\nconsider restricting access using GRANT/REVOKE.\n\nYou can restrict access to the database itself to just the +app+ account.  You\ncan run this using the +app+ account, since that account owns the database:\n\n  GRANT ALL ON DATABASE ${DATABASE_NAME} TO ${DATABASE_NAME};\n  REVOKE ALL ON DATABASE ${DATABASE_NAME} FROM public;\n\nYou can also restrict access to the public schema (this is not needed if you\nare using a custom schema).  Note that by default, the database superuser\nowns the public schema, so you have to run this as the database superuser\naccount (generally +postgres+):\n\n  GRANT ALL ON SCHEMA public TO ${DATABASE_NAME};\n  GRANT USAGE ON SCHEMA public TO ${DATABASE_NAME}_password;\n  REVOKE ALL ON SCHEMA public FROM public;\n\nIf you are using MySQL or Microsoft SQL Server, please consult their\ndocumentation for how to restrict access so that the +ph+ account cannot\nlogin directly.\n\n== Usage\n\n=== Basic Usage\n\nRodauth is a Roda plugin and loaded the same way other Roda plugins\nare loaded:\n\n  plugin :rodauth do\n  end\n\nThe block passed to the plugin call uses the Rodauth configuration DSL.\nThe one configuration method that should always be used is +enable+,\nwhich chooses which features you would like to load:\n\n  plugin :rodauth do\n    enable :login, :logout\n  end\n\nOnce features are loaded, you can use any of the configuration methods\nsupported by the features.  There are two types of configuration\nmethods.  The first type are called auth methods, and they take a\nblock which overrides the default method that Rodauth uses.  Inside the\nblock, you can call super if you want to get the default behavior, though\nyou must provide explicit arguments to super.  There is no need to\ncall super in before or after hooks, though. For example, if you want to\nadd additional logging when a user logs in:\n\n  plugin :rodauth do\n    enable :login, :logout\n    after_login do\n      LOGGER.info \"#{account[:email]} logged in!\"\n    end\n  end\n\nInside the block, you are in the context of the Rodauth::Auth\ninstance related to the request.  This object has access to everything\nrelated to the request via methods:\n\nrequest :: RodaRequest instance\nresponse :: RodaResponse instance\nscope :: Roda instance\nsession :: session hash\nflash :: flash message hash\naccount :: account hash (if set by an earlier Rodauth method)\ncurrent_route :: route name symbol (if Rodauth is handling the route)\n\nSo if you want to log the IP address for the user during login:\n\n  plugin :rodauth do\n    enable :login, :logout\n    after_login do\n      LOGGER.info \"#{account[:email]} logged in from #{request.ip}\"\n    end\n  end\n\nThe second type of configuration methods are called auth value\nmethods.  They are similar to auth methods, but instead of just\naccepting a block, they can optionally accept a single argument\nwithout a block, which will be treated as a block that just returns\nthat value.  For example, the accounts_table method sets the database\ntable storing accounts, so to override it, you can call the method\nwith a symbol for the table:\n\n  plugin :rodauth do\n    enable :login, :logout\n    accounts_table :users\n  end\n\nNote that all auth value methods can still take a block, allowing\noverriding for all behavior, using any information from the request:\n\n  plugin :rodauth do\n    enable :login, :logout\n    accounts_table do\n      request.ip.start_with?(\"192.168.1.\") ? :admins : :users\n    end\n  end\n\nBy allowing every configuration method to take a block, Rodauth\nshould be flexible enough to integrate into most legacy systems.\n\n=== Plugin Options\n\nWhen loading the rodauth plugin, you can also pass an options hash,\nwhich configures which dependent plugins should be loaded. Options:\n\n:csrf :: Set to +false+ to not load a csrf plugin.  Set to +:rack_csrf+\n         to use the csrf plugin instead of the route_csrf plugin.\n:flash :: Set to +false+ to not load the flash plugin\n:render :: Set to +false+ to not load the render plugin. This is useful\n           to avoid the dependency on tilt when using alternative view libraries.\n:json :: Set to +true+ to load the json and json_parser plugins. Set\n         to +:only+ to only load those plugins and not any other plugins.\n         Note that if you are enabling features that send email, you\n         still need to load the render plugin manually.\n:name :: Provide a name for the given Rodauth configuration, used to\n         support multiple Rodauth configurations in a given Roda application.\n:auth_class :: Provide a specific Rodauth::Auth subclass that should be set\n               on the Roda application. By default, an anonymous\n               Rodauth::Auth subclass is created.\n\n=== Feature Documentation\n\nThe options/methods for the supported features are listed on a\nseparate page per feature.  If these links are not active, please\nview the appropriate file in the doc directory.\n\n* {Base}[rdoc-ref:doc/base.rdoc] (this feature is autoloaded)\n* {Login Password Requirements Base}[rdoc-ref:doc/login_password_requirements_base.rdoc] (this feature is autoloaded by features that set logins/passwords)\n* {Email Base}[rdoc-ref:doc/email_base.rdoc] (this feature is autoloaded by features that send email)\n* {Two Factor Base}[rdoc-ref:doc/two_factor_base.rdoc] (this feature is autoloaded by 2 factor authentication features)\n* {Account Expiration}[rdoc-ref:doc/account_expiration.rdoc]\n* {Active Sessions}[rdoc-ref:doc/active_sessions.rdoc]\n* {Audit Logging}[rdoc-ref:doc/audit_logging.rdoc]\n* {Argon2}[rdoc-ref:doc/argon2.rdoc]\n* {Change Login}[rdoc-ref:doc/change_login.rdoc]\n* {Change Password}[rdoc-ref:doc/change_password.rdoc]\n* {Change Password Notify}[rdoc-ref:doc/change_password_notify.rdoc]\n* {Close Account}[rdoc-ref:doc/close_account.rdoc]\n* {Confirm Password}[rdoc-ref:doc/confirm_password.rdoc]\n* {Create Account}[rdoc-ref:doc/create_account.rdoc]\n* {Disallow Common Passwords}[rdoc-ref:doc/disallow_common_passwords.rdoc]\n* {Disallow Password Reuse}[rdoc-ref:doc/disallow_password_reuse.rdoc]\n* {Email Authentication}[rdoc-ref:doc/email_auth.rdoc]\n* {HTTP Basic Auth}[rdoc-ref:doc/http_basic_auth.rdoc]\n* {Internal Request}[rdoc-ref:doc/internal_request.rdoc]\n* {JSON}[rdoc-ref:doc/json.rdoc]\n* {JWT CORS}[rdoc-ref:doc/jwt_cors.rdoc]\n* {JWT Refresh}[rdoc-ref:doc/jwt_refresh.rdoc]\n* {JWT}[rdoc-ref:doc/jwt.rdoc]\n* {Lockout}[rdoc-ref:doc/lockout.rdoc]\n* {Login}[rdoc-ref:doc/login.rdoc]\n* {Logout}[rdoc-ref:doc/logout.rdoc]\n* {OTP}[rdoc-ref:doc/otp.rdoc]\n* {OTP Lockout Email}[rdoc-ref:doc/otp_lockout_email.rdoc]\n* {OTP Modify Email}[rdoc-ref:doc/otp_modify_email.rdoc]\n* {OTP Unlock}[rdoc-ref:doc/otp_unlock.rdoc]\n* {Password Complexity}[rdoc-ref:doc/password_complexity.rdoc]\n* {Password Expiration}[rdoc-ref:doc/password_expiration.rdoc]\n* {Password Grace Period}[rdoc-ref:doc/password_grace_period.rdoc]\n* {Password Pepper}[rdoc-ref:doc/password_pepper.rdoc]\n* {Path Class Methods}[rdoc-ref:doc/path_class_methods.rdoc]\n* {Recovery Codes}[rdoc-ref:doc/recovery_codes.rdoc]\n* {Remember}[rdoc-ref:doc/remember.rdoc]\n* {Reset Password}[rdoc-ref:doc/reset_password.rdoc]\n* {Reset Password Notify}[rdoc-ref:doc/reset_password_notify.rdoc]\n* {Session Expiration}[rdoc-ref:doc/session_expiration.rdoc]\n* {Single Session}[rdoc-ref:doc/single_session.rdoc]\n* {SMS Codes}[rdoc-ref:doc/sms_codes.rdoc]\n* {Update Password Hash}[rdoc-ref:doc/update_password_hash.rdoc]\n* {Verify Account}[rdoc-ref:doc/verify_account.rdoc]\n* {Verify Account Grace Period}[rdoc-ref:doc/verify_account_grace_period.rdoc]\n* {Verify Login Change}[rdoc-ref:doc/verify_login_change.rdoc]\n* {WebAuthn}[rdoc-ref:doc/webauthn.rdoc]\n* {WebAuthn Autofill}[rdoc-ref:doc/webauthn_autofill.rdoc]\n* {WebAuthn Login}[rdoc-ref:doc/webauthn_login.rdoc]\n* {WebAuthn Modify Email}[rdoc-ref:doc/webauthn_modify_email.rdoc]\n* {WebAuthn Verify Account}[rdoc-ref:doc/webauthn_verify_account.rdoc]\n\n=== Calling Rodauth in the Routing Tree\n\nIn general, you will usually want to call +r.rodauth+ early in your\nroute block:\n\n  route do |r|\n    r.rodauth\n\n    # ...\n  end\n\nNote that will allow Rodauth to run, but it won't force people\nto login or add any security to your site.  If you want to force\nall users to login, you need to redirect to them login page if\nthey are not already logged in:\n\n  route do |r|\n    r.rodauth\n    rodauth.require_authentication\n\n    # ...\n  end\n\nIf only certain parts of your site require logins, then you can\nonly redirect if they are not logged in certain branches of the\nrouting tree:\n\n  route do |r|\n    r.rodauth\n\n    r.on \"admin\" do\n      rodauth.require_authentication\n\n      # ...\n    end\n\n    # ...\n  end\n\nIn some cases you may want to have rodauth run inside a branch of\nthe routing tree, instead of in the root.  You can do this by\nsetting a +:prefix+ when configuring Rodauth, and calling +r.rodauth+\ninside a matching routing tree branch:\n\n  plugin :rodauth do\n    enable :login, :logout\n    prefix \"/auth\"\n  end\n\n  route do |r|\n    r.on \"auth\" do\n      r.rodauth\n    end\n\n    rodauth.require_authentication\n\n    # ...\n  end\n\n=== +rodauth+ Methods\n\nMost of Rodauth's functionality is exposed via +r.rodauth+, which allows\nRodauth to handle routes for the features you have enabled (such as +/login+\nfor login).  However, as you have seen above, you may want to call methods on\nthe +rodauth+ object, such as for checking if the current request has been\nauthenticated.\n\nHere are methods designed to be callable on the +rodauth+ object outside\n+r.rodauth+:\n\nrequire_login :: Require the session be logged in, redirecting the request to the\n                 login page if the request has not been logged in.\nrequire_authentication :: Similar to +require_login+, but also requires\n                          two factor authentication if the account has setup\n                          two factor authentication.  Redirects the request to\n                          the two factor authentication page if logged in but not\n                          authenticated via two factors.\nrequire_account :: Similar to +require_authentication+, but also loads the logged\n                   in account to ensure it exists in the database.  If the account\n                   doesn't exist, or if it exists but isn't verified, the session\n                   is cleared and the request redirected to the login page.\nlogged_in? :: Whether the session has been logged in.\nauthenticated? :: Similar to +logged_in?+, but if the account has setup two\n                  factor authentication, whether the session has authenticated\n                  via two factors.\naccount! :: Returns the current account record if it has already been loaded,\n            otherwise retrieves the account from session if logged in.\nauthenticated_by :: An array of strings for successful authentication methods for\n                    the current session (e.g. password/remember/webauthn).\npossible_authentication_methods :: An array of strings for possible authentication\n                                   types that can be used for the account.\nautologin_type :: If the current session was authenticated via autologin, the\n                  type of autologin used.\nrequire_two_factor_setup :: (two_factor_base feature) Require the session to have\n                            setup two factor authentication, redirecting the\n                            request to the two factor authentication setup page\n                            if not.\ntwo_factor_partially_authenticated? :: (two_factor_base feature) Returns true if\n                                       the session is logged in, the account has\n                                       setup two factor authentication, but has\n                                       not yet authenticated with a second factor.\nuses_two_factor_authentication? :: (two_factor_base feature) Whether the account\n                                   for the current session has setup two factor\n                                   authentication.\nupdate_last_activity :: (account_expiration feature) Update the last activity\n                        time for the current account.  Only makes sense to use\n                        this if you are expiring accounts based on last activity.\nrequire_current_password :: (password_expiration feature) Require a current\n                            password, redirecting the request to the change\n                            password page if the password for the account has\n                            expired.\nrequire_password_authentication :: (confirm_password feature) If not authenticated\n                                   via password and the account has a password,\n                                   redirect to the password confirmation page,\n                                   saving the current location to redirect back\n                                   to after password has been successfully\n                                   confirmed. If the password_grace_period feature\n                                   is used, also redirect if the password has not\n                                   been recently entered.\nload_memory :: (remember feature) If the session has not been authenticated, look\n               for the remember cookie.  If present and valid, automatically\n               log the session in, but mark that it was logged in via a remember\n               key.\nlogged_in_via_remember_key? :: (remember feature) Whether the current session has\n                               been logged in via a remember key.  For security\n                               sensitive actions where you want to require the user\n                               to reenter the password, you can use the\n                               confirm_password feature.\nhttp_basic_auth :: (http_basic_auth feature) Use HTTP Basic Authentication information\n                   to login the user if provided.\nrequire_http_basic_auth :: (http_basic_auth feature) Require that HTTP Basic\n                           Authentication be provided in the request.\ncheck_session_expiration :: (session_expiration feature) Check whether the current\n                            session has expired, automatically logging the session\n                            out if so.\ncheck_active_session :: (active_sessions feature) Check whether the current session\n                        is still active, automatically logging the session out if not.\ncheck_single_session :: (single_session feature) Check whether the current\n                        session is still the only valid session, automatically logging\n                        the session out if not.\nverified_account? :: (verify_grace_period feature) Whether the account is currently\n                     verified.  If false, it is because the account is allowed to\n                     login as they are in the grace period.\nlocked_out? :: (lockout feature) Whether the account for the current session has been\n               locked out.\nauthenticated_webauthn_id :: (webauthn feature) If the current session was\n                             authenticated via webauthn, the webauthn id of the\n                             credential used.\n*_path :: One of these is added for each of the routes added by Rodauth, giving the\n          relative path to the route. Any options passed to this method will be\n          converted into query parameters.\n*_url :: One of these is added for each of the routes added by Rodauth, giving the\n         URL to the route. Any options passed to this method will be converted\n         into query parameters.\n\n=== Calling Rodauth Methods for Other Accounts\n\nIn some cases, you may want to interact with Rodauth directly on behalf\nof a user.  For example, let's say you want to create accounts or change passwords\nfor existing accounts.  Using Rodauth's internal_request feature, you can do this\nby:\n\n  plugin :rodauth do\n    enable :create_account, :change_password, :internal_request\n  end\n  rodauth.create_account(login: 'foo@example.com', password: '...')\n  rodauth.change_password(account_id: 24601, password: '...')\n\nHere the +rodauth+ method is called as the Roda class level, which returns\nthe appropriate \u003ctt\u003eRodauth::Auth\u003c/tt\u003e subclass.  You call internal request\nmethods on that class to perform actions on behalf of a user. See the\n{internal request feature documentation}[rdoc-ref:doc/internal_request.rdoc]\nfor details.\n\n== Using Rodauth as a Library\n\nRodauth was designed to serve as an authentication framework for Rack applications.\nHowever, Rodauth can be used purely as a library outside of a web application. You\ncan do this by requiring +rodauth+, and using the +Rodauth.lib+ method to return\na \u003ctt\u003eRodauth::Auth\u003c/tt\u003e subclass, which you can call methods on.  You pass the\n+Rodauth.lib+ method an optional hash of Rodauth plugin options and a Rodauth\nconfiguration block:\n\n  require 'rodauth'\n  rodauth = Rodauth.lib do\n    enable :create_account, :change_password\n  end\n  rodauth.create_account(login: 'foo@example.com', password: '...')\n  rodauth.change_password(account_id: 24601, password: '...')\n\nThis supports builds on top of the internal_request support (it implicitly loads\nthe internal_request feature before processing the configuration block), and\nallows the use of Rodauth in non-web applications. Note that you still have to\nsetup a Sequel::Database connection for Rodauth to use for data storage.\n\n=== With Multiple Configurations\n\nRodauth supports using multiple rodauth configurations in the same\napplication.  You just need to load the plugin a second time,\nproviding a name for any alternate configuration:\n\n  plugin :rodauth do\n  end\n  plugin :rodauth, name: :secondary do\n  end\n\nThen in your routing code, any time you call rodauth, you can provide\nthe name as an argument to use that configuration:\n\n  route do |r|\n    r.on 'secondary' do\n      r.rodauth(:secondary)\n    end\n\n    r.rodauth\n  end\n\nTo prevent having to specify the name manually in all cases where you are\nusing it, you can define a +default_rodauth_name+ method in your Roda\napplication that returns the name that should be used:\n\n  attr_reader :default_rodauth_name\n\n  route do |r|\n    r.on 'secondary' do\n      @default_rodauth_name = :secondary\n      r.rodauth # will use the :secondary configuration\n    end\n\n    r.rodauth # will use the default configuration\n  end\n\nBy default, alternate configurations will use the same session keys as the\nprimary configuration, which may be undesirable. To ensure session state is\nseparated between configurations, you can set a session key prefix for\nalternate configurations.  If you are using the remember feature in both\nconfigurations, you may also want to set a different remember key in the\nalternate configuration:\n\n  plugin :rodauth, name: :secondary do\n    session_key_prefix \"secondary_\"\n    remember_cookie_key \"_secondary_remember\"\n  end\n\n=== With Password Hashes Inside the Accounts Table\n\nYou can use Rodauth if you are storing password hashes in the same\ntable as the accounts.  You just need to specify which column\nstores the password hash:\n\n  plugin :rodauth do\n    account_password_hash_column :password_hash\n  end\n\nWhen this option is set, Rodauth will do the password hash check\nin ruby.\n  \n=== When Using PostgreSQL/MySQL/Microsoft SQL Server without Database Functions\n\nIf you want to use Rodauth on PostgreSQL, MySQL, or Microsoft SQL Server\nwithout using database functions for authentication, but still storing password\nhashes in a separate table, you can do so:\n\n  plugin :rodauth do\n    use_database_authentication_functions? false\n  end\n\nConversely, if you implement the rodauth_get_salt and\nrodauth_valid_password_hash functions on a database that isn't\nPostgreSQL, MySQL, or Microsoft SQL Server, you can set this value to true.\n\n=== With Custom Authentication\n\nYou can use Rodauth with other authentication types, by using some\nof Rodauth's configuration methods.\n\nNote that when using custom authentication, using some of Rodauth's\nfeatures such as change login and change password either would not\nmake sense or would require some additional custom configuration.\nThe login and logout features should work correctly with the examples\nbelow, though.\n\n==== Using LDAP Authentication\n\nIf you have accounts stored in the database, but authentication happens\nvia LDAP, you can use the +simple_ldap_authenticator+ library:\n\n  require 'simple_ldap_authenticator'\n  plugin :rodauth do\n    enable :login, :logout\n    require_bcrypt? false\n    password_match? do |password|\n      SimpleLdapAuthenticator.valid?(account[:email], password)\n    end\n  end\n\nIf you aren't storing accounts in the database, but want to allow\nany valid LDAP user to login, you can do something like this:\n\n  require 'simple_ldap_authenticator'\n  plugin :rodauth do\n    enable :login, :logout\n\n    # Don't require the bcrypt library, since using LDAP for auth\n    require_bcrypt? false\n\n    # Store session value in :login key, since the :account_id\n    # default wouldn't make sense\n    session_key :login\n\n    # Use the login provided as the session value\n    account_session_value{account}\n\n    # Treat the login itself as the account\n    account_from_login{|l| l.to_s}\n\n    password_match? do |password|\n      SimpleLdapAuthenticator.valid?(account, password)\n    end\n  end\n\n==== Using Facebook Authentication\n\nHere's an example of authentication using Facebook with a JSON API.\nThis setup assumes you have client-side code to submit JSON POST requests\nto +/login+ with an +access_token+ parameter that is set to the user's\nFacebook OAuth access token.\n\n\n  require 'koala'\n  plugin :rodauth do\n    enable :login, :logout, :jwt\n     \n    require_bcrypt? false\n    session_key :facebook_email\n    account_session_value{account}\n\n    login_param 'access_token'\n\n    account_from_login do |access_token|\n      fb = Koala::Facebook::API.new(access_token)\n      if me = fb.get_object('me', fields: [:email])\n        me['email']\n      end\n    end\n\n    # there is no password!\n    password_match? do |pass|\n      true\n    end\n  end\n\n=== With Rails\n\nIf you're using Rails, you can use the\n{rodauth-rails}[https://github.com/janko/rodauth-rails] gem which provides\nRails integration for Rodauth. Some of its features include:\n\n* generators for Rodauth \u0026 Sequel configuration, as well as views and mailers\n* uses Rails' flash messages and CSRF protection\n* automatically sets HMAC secret to Rails' secret key base\n* uses Action Controller \u0026 Action View for rendering templates\n* uses Action Mailer for sending emails\n\nFollow the instructions in the rodauth-rails README to get started.\n\n=== With Other Web Frameworks\n\nYou can use Rodauth even if your application does not use the Roda web\nframework.  This is possible by adding a Roda middleware that uses\nRodauth:\n\n  require 'roda'\n\n  class RodauthApp \u003c Roda\n    plugin :middleware\n    plugin :rodauth do\n      enable :login\n    end\n\n    route do |r|\n      r.rodauth\n      rodauth.require_authentication\n      env['rodauth'] = rodauth\n    end\n  end\n\n  use RodauthApp\n\nNote that Rodauth expects the Roda app it is used in to provide a\nlayout.  So if you are using Rodauth as middleware for another app,\nif you don't have a +views/layout.erb+ file that Rodauth can use,\nyou should probably also add load Roda's +render+ plugin\nwith the appropriate settings that allow Rodauth to use the same\nlayout as the application.\n\nBy setting \u003ctt\u003eenv['rodauth'] = rodauth\u003c/tt\u003e in the route block\ninside the middleware, you can easily provide a way for your\napplication to call Rodauth methods.\n\nIf you're using the remember feature with +extend_remember_deadline?+ set to\ntrue, you'll want to load roda's middleware plugin with\n\u003ctt\u003eforward_response_headers: true\u003c/tt\u003e option, so that +Set-Cookie+ header changes\nfrom the +load_memory+ call in the route block are propagated when the request\nis forwarded to the main app.\n\nHere are some examples of integrating Rodauth into applications that\ndon't use Roda:\n\n* {Ginatra, a Sinatra-based git repository viewer}[https://github.com/jeremyevans/ginatra/commit/28108ebec96e8d42596ee55b01c3f7b50c155dd1] \n* {Rodauth's demo site as a Rails 6 application}[https://github.com/janko/rodauth-demo-rails]\n* {Grape application}[https://github.com/davydovanton/grape-rodauth]\n* {Hanami application}[https://github.com/davydovanton/rodauth_hanami]\n\n=== Using 2 Factor Authentication\n\nRodauth ships with 2 factor authentication support via the following\nmethods:\n\n* WebAuthn\n* TOTP (Time-Based One-Time Passwords, RFC 6238).\n* SMS Codes\n* Recovery Codes\n\nThere are multiple ways to integrate 2 factor authentication with\nRodauth, based on the needs of the application.  By default, SMS\ncodes and recovery codes are treated only as backup 2nd factors,\na user cannot enable them without first enabling another 2nd factor\nauthentication method.  However, you can change this by using\na configuration method.\n\nIf you want to support but not require 2 factor authentication:\n\n  plugin :rodauth do\n    enable :login, :logout, :otp, :recovery_codes, :sms_codes\n  end\n  route do |r|\n    r.rodauth\n    rodauth.require_authentication\n\n    # ...\n  end\n\nIf you want to force all users to use 2 factor authentication, requiring users\nthat don't currently have two authentication to set it up:\n\n  route do |r|\n    r.rodauth\n    rodauth.require_authentication\n    rodauth.require_two_factor_setup\n\n    # ...\n  end\n\nSimilarly to requiring authentication in general, it's possible to require\nlogin authentication for most of the site, but require 2 factor\nauthentication only for particular branches:\n\n  route do |r|\n    r.rodauth\n    rodauth.require_login\n\n    r.on \"admin\" do\n      rodauth.require_two_factor_authenticated\n    end\n\n    # ...\n  end\n\n=== JSON API Support\n\nTo add support for handling JSON responses, you can pass the +:json+\noption to the plugin, and enable the JWT feature in addition to\nother features you plan to use:\n\n  plugin :rodauth, json: true do\n    enable :login, :logout, :jwt\n  end\n\nIf you do not want to load the HTML plugins that Rodauth usually loads\n(render, csrf, flash, h), because you are building a JSON-only API,\npass \u003ctt\u003e:json =\u003e :only\u003c/tt\u003e\n\n  plugin :rodauth, json: :only do\n    enable :login, :logout, :jwt\n  end\n\nNote that by default, the features that send email depend on the\nrender plugin, so if using the \u003ctt\u003ejson: :only\u003c/tt\u003e option, you\neither need to load the render plugin manually or you need to\nuse the necessary *_email_body configuration options to specify\nthe body of the emails.\n\nThe JWT feature enables JSON API support for all of the other features\nthat Rodauth ships with. If you would like JSON API access that still uses\nrack session for storing session data, enable the JSON feature instead:\n\n  plugin :rodauth, json: true do\n    enable :login, :logout, :json\n    only_json? true # if you want to only handle JSON requests\n  end\n\n=== Adding Custom Methods to the +rodauth+ Object\n\nInside the configuration block, you can use +auth_class_eval+ to add\ncustom methods that will be callable on the +rodauth+ object.\n\n  plugin :rodauth do\n    enable :login\n\n    auth_class_eval do\n      def require_admin\n        request.redirect(\"/\") unless account[:admin]\n      end\n    end\n  end\n\n  route do |r|\n    r.rodauth\n\n    r.on \"admin\" do\n      rodauth.require_admin\n    end\n  end\n\n=== Using External Features\n\nThe +enable+ configuration method is able to load features external to\nRodauth.  You need to place the external feature file where it can be\nrequired via rodauth/features/feature_name. That file should\nuse the following basic structure\n\n  module Rodauth\n    # :feature_name will be the argument given to enable to\n    # load the feature, :FeatureName is optional and will be used to\n    # set a constant name for prettier inspect output.\n    Feature.define(:feature_name, :FeatureName) do\n      # Shortcut for defining auth value methods with static values\n      auth_value_method :method_name, 1 # method_value\n\n      auth_value_methods # one argument per auth value method\n\n      auth_methods # one argument per auth method\n \n      route do |r|\n        # This block is taken for requests to the feature's route.\n        # This block is evaluated in the scope of the Rodauth::Auth instance.\n        # r is the Roda::RodaRequest instance for the request\n        \n        r.get do\n        end\n\n        r.post do\n        end\n      end\n\n      configuration_module_eval do\n        # define additional configuration specific methods here, if any\n      end\n\n      # define the default behavior for the auth_methods\n      # and auth_value_methods\n      # ...\n    end\n  end\n\nSee the {internals guide}[rdoc-ref:doc/guides/internals.rdoc] for a more complete\nexample of how to construct features.\n\n=== Overriding Route-Level Behavior\n\nAll of Rodauth's configuration methods change the behavior of the\nRodauth::Auth instance.  However, in some cases you may want to\noverriding handling at the routing layer.  You can do this easily\nby adding an appropriate route before calling +r.rodauth+:\n\n  route do |r|\n    r.post 'login' do\n      # Custom POST /login handling here\n    end\n\n    r.rodauth\n  end\n\n=== Precompiling Rodauth Templates\n\nRodauth serves templates from it's gem folder.  If you are using\na forking webserver and want to preload the compiled templates\nto save memory, or if you are chrooting your application, you can\nbenefit from precompiling your rodauth templates:\n\n  plugin :rodauth do\n    # ...\n  end\n  precompile_rodauth_templates\n\n== Ruby Support Policy\n\nRodauth fully supports the currently supported versions of Ruby (MRI) and JRuby.  It may\nsupport unsupported versions of Ruby or JRuby, but such support may be dropped in any\nminor version if keeping it becomes a support issue.  The minimum Ruby version\nrequired to run the current version of Rodauth is 1.9.2.\n\n== Similar Projects\n\nAll of these are Rails-specific:\n\n* {Devise}[https://github.com/heartcombo/devise]\n* {Authlogic}[https://github.com/binarylogic/authlogic]\n* {Sorcery}[https://github.com/Sorcery/sorcery]\n\n== Author\n\nJeremy Evans \u003ccode@jeremyevans.net\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeremyevans%2Frodauth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeremyevans%2Frodauth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeremyevans%2Frodauth/lists"}