{"id":13880474,"url":"https://github.com/airblade/quo_vadis","last_synced_at":"2026-02-28T01:36:36.344Z","repository":{"id":1344887,"uuid":"1291112","full_name":"airblade/quo_vadis","owner":"airblade","description":"Multifactor authentication for Rails.","archived":false,"fork":false,"pushed_at":"2025-11-18T12:56:23.000Z","size":527,"stargazers_count":34,"open_issues_count":11,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-03T13:52:28.869Z","etag":null,"topics":["authentication","rails","rails-authentication","two-factor-authentication"],"latest_commit_sha":null,"homepage":"","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/airblade.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2011-01-25T10:58:02.000Z","updated_at":"2025-11-18T12:56:26.000Z","dependencies_parsed_at":"2022-07-07T02:03:50.140Z","dependency_job_id":"5167b956-f7df-4db0-ae27-4ff1de3834e5","html_url":"https://github.com/airblade/quo_vadis","commit_stats":{"total_commits":216,"total_committers":2,"mean_commits":108.0,"dds":0.00462962962962965,"last_synced_commit":"6a43c38478104f758bdfa926324a9a1ebea87d6c"},"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"purl":"pkg:github/airblade/quo_vadis","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/airblade%2Fquo_vadis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/airblade%2Fquo_vadis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/airblade%2Fquo_vadis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/airblade%2Fquo_vadis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/airblade","download_url":"https://codeload.github.com/airblade/quo_vadis/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/airblade%2Fquo_vadis/sbom","scorecard":{"id":173353,"data":{"date":"2025-08-11","repo":{"name":"github.com/airblade/quo_vadis","commit":"2abc56694df56f238fce67f413d5cb2d56eaed81"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-16T17:07:40.804Z","repository_id":1344887,"created_at":"2025-08-16T17:07:40.804Z","updated_at":"2025-08-16T17:07:40.804Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29922186,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-27T19:37:42.220Z","status":"ssl_error","status_checked_at":"2026-02-27T19:37:41.463Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["authentication","rails","rails-authentication","two-factor-authentication"],"created_at":"2024-08-06T08:03:04.132Z","updated_at":"2026-02-28T01:36:36.326Z","avatar_url":"https://github.com/airblade.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Quo Vadis\n\nMultifactor authentication for your Rails app (backwards-compatible to Rails 6).\n\nDesigned in accordance with the [OWASP Application Security Verification Standard](https://owasp.org/www-project-application-security-verification-standard/) and relevant [OWASP Cheatsheets](https://cheatsheetseries.owasp.org).\n\nSimple to integrate into your application.  The main task is customising the example views' markup to match your look-and-feel.\n\n\n## Features\n\n### General features\n\n- Works with any model, e.g. `User` or `Person`.\n- Works with multiple models, e.g. `User` and `Admin`.\n- Works with any identifier, e.g. `:username` or `:email`.\n- Minimal footprint in your models and controllers.\n- Does not touch your existing database tables.\n- Secrets (password, TOTP secret, 2FA recovery codes) are encrypted at rest.\n\n### Authentication features\n\n- Authentication by password.\n- Two-factor authentication (2FA) by TOTP with recovery codes as a backup factor.  Can be optional or mandatory.\n- Change password.\n- Reset password.\n- Account confirmation (a.k.a. email verification) (optional).\n- OTPs (account confirmation, password reset), TOTPs, and recovery codes are all one-time-only.\n- Sessions expired after lifetime or idle time exceeded.\n- Session replaced after any privilege change.\n- View active sessions, log out of any of them.\n- Email-notifications of updates to authentication details.\n- Audit trail.\n\n### Testing\n\n- Can shortcut logging in for speedier tests.\n\n\n## Installation\n\nAdd the gem to your Gemfile:\n\n```ruby\nbundle add 'quo_vadis'\n```\n\nNext, add the database tables:\n\n```\nrails quo_vadis:install:migrations \u0026\u0026 rails db:migrate\n```\n\nAll the database tables are prefixed with `qv_`.\n\nFinally, copy the example views across:\n\n```\nrails generate quo_vadis:install\n```\n\nYou may find that you need to eager load your code in development so that your model's / models' `#authenticates` call(s) executes straightaway.  This method registers your model(s) with QuoVadis, which needs to happen before you can do any authentication-related things such as reset your password.\n\n\n## Usage\n\n\n### Model\n\nYour model must have an `:email` attribute.  All authentication-related emails will be sent to this address.\n\nYour model must have an identifier, e.g. `:email` (default) or `:username`, with a uniqueness validation.\n\nAll you need do is add a call to `authenticates`, somewhere after your identifier's uniqueness validation.\n\nFor example, let's say you have a `User` model and the identifier is `:email`:\n\n```ruby\nclass User \u003c ApplicationRecord\n  validates :email, uniqueness: {case_sensitive: false}\n  authenticates\nend\n```\n\nIf instead you had a `Person` model with a `:username` identifier:\n\n```ruby\nclass Person \u003c ApplicationRecord\n  validates :username, uniqueness: {case_sensitive: false}\n  authenticates identifier: :username\nend\n```\n\nYou can create and update your models as before.  When you want to set a password for the first time, just include `:password` and, optionally, `:password_confirmation` in the attributes to `#create` or `#update`.\n\nIf you want to change an existing password, use the [Change Password](#change-password) feature.  If you update a model (that already has a password) with a `:password` attribute, it will raise a `QuoVadis::PasswordExistsError`.\n\nThe minimum password length is configured by `QuoVadis.password_minimum_length` (12 by default).\n\n\n### Controllers\n\nYou can use these methods in your controllers.\n\n#### `require_password_authentication`\n\nUse this to restrict actions to password-authenticated users.  It is aliased to `:require_authentication` for convenience.\n\n```ruby\nclass FoosController \u003c ApplicationController\n  before_action :require_password_authentication\nend\n```\n\n#### `require_two_factor_authentication`\n\nUse this to restrict actions to users authenticated with both a password and a second factor.  (You do not need to use `:require_password_authentication` for these actions.)\n\n```ruby\nclass BarsController \u003c ApplicationController\n  before_action :require_two_factor_authentication\nend\n```\n\n#### `login(model, browser_session = true, metadata: {})`\n\nUse this to log in a user who has authenticated with a password.  For the optional `browser_session` argument, pass `true` to log in for the duration of the browser session, or `false` to log in for `QuoVadis.session_lifetime` (which could be the browser session anyway).  Any metadata are stored in the log entry for the login.\n\n#### `authenticated_model`\n\nCall this to get the authenticated user.  Feel free to alias this to `:current_user` or set it into an `ActiveSupport::CurrentAttributes` class.\n\nAvailable in controllers and views.\n\n#### `logged_in?`\n\nCall this to find out whether a user has authenticated with a password.\n\nAvailable in controllers and views.\n\n\n### Routes\n\nYou can use routing constraints to restrict routes to logged-in or logged-out users.  For example:\n\n```ruby\nRails.application.routes.draw do\n  constraints(QuoVadis::Constraints::LoggedOut) do\n    root \"pages#index\"\n  end\n\n  constraints(QuoVadis::Constraints::LoggedIn) do\n    root \"dashboard#show\", as: :dashboard\n  end\nend\n```\n\n\n### Views\n\nYou can use `authenticated_model` and `logged_in?` in your views.  For example:\n\n```erb\n\u003c% if logged_in? %\u003e\n  \u003c%= link_to 'My profile', authenticated_model %\u003e\n\u003c% end %\u003e\n```\n\nIn your own views, you must prefix QuoVadis's routes with `quo_vadis.`.  For example:\n\n```ruby\nlink_to 'Log in', quo_vadis.login_path\n```\n\nWhen you are customising QuoVadis's views, you must prefix your app's routes with `main_app.`.  For example:\n\n```ruby\nlink_to 'Home', main_app.root_path\n```\n\n\n## Features\n\nThe example views show the forms and fields you need.  You should only need to adapt the markup to suit your app's appearance.\n\nIn the snippets below we assume a `User` model whose identifier is `:email`.  You can of course use anything you like.\n\n\n### Sign up\n\nYour new user sign-up form ([example](https://github.com/airblade/quo_vadis/blob/master/test/dummy/app/views/users/new.html.erb)) must include:\n\n- a `:password` field;\n- optionally a `:password_confirmation` field;\n- a field for their identifier;\n- an `:email` field if the identifier is not their email.\n\nIn your controller, use the [`#login`](#loginmodel-browser_session--true-metadata-) method to log in your new user.  The optional second argument specifies for how long the user should be logged in, and any metadata you supply is logged in the audit log.\n\nAfter logging in the user, redirect them wherever you like as normal.\n\n```ruby\nclass UsersController \u003c ApplicationController\n  def create\n    @user = User.new user_params\n    if @user.save\n      login @user  # \u003c-- add this\n      redirect_to dashboard_path\n    else\n      # ...\n    end\n  end\n\n  private\n\n  def user_params\n    params.require(:user).permit(:name, :email, :password, :password_confirmation)\n  end\nend\n```\n\n### Sign up with account confirmation\n\nFollow the steps above for sign-up.\n\nAfter you have logged in the user and redirected them, QuoVadis detects that they need to confirm their account.  QuoVadis emails them a 6-digit confirmation code and redirects them to the confirmation page where they can enter that code.\n\nThe confirmation code is valid for `QuoVadis.account_confirmation_otp_lifetime`.\n\nOnce the user has confirmed their account, they will be redirected to the page they requested before they were redirected to the confirmation page.\n\nYou need to write the email view ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/account_confirmation.text.erb)).  It must be in `app/views/quo_vadis/mailer/account_confirmation.{text,html}.erb` and output the `@otp` variable.  See the [Configuration](#configuration) section for how to set QuoVadis's emails' from addresses, headers, etc.\n\nNow write the confirmation page where the user types in the confirmation code from the email ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/confirmations/new.html.erb)).  It must be in `app/views/quo_vadis/confirmations/new.html.:format` and must POST the `otp` field to `confirm_path`.  You can provide a button to send a new confirmation code (perhaps the original email didn't arrive, or the user didn't have time to act on it before it expired) – it should POST to `send_confirmation_path`.\n\nIf the user closes their browser after signing up but before they have confirmed their account, when they next access a page which requires being logged in they will be sent a new confirmation code and redirected to the confirmation page, as if they had just signed up.\n\n\n### Login\n\nUse `before_action :require_password_authentication` or `before_action :require_authentication` in your controllers.\n\nWrite the login view ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/sessions/new.html.erb)).  Your login form must be in `app/views/quo_vadis/sessions/new.html.:format`.  Note it must capture the user's identifier (not email, unless the identifier is email).\n\nIf you include a `remember` checkbox in your login form:\n\n- if the user checks it, they will be logged in for `QuoVadis.session_lifetime`;\n- if the user does not check it, they will be logged in for the browser session.\n\nIf you do not include a `remember` checkbox, the user will be logged in for `QuoVadis.session_lifetime`.\n\nAfter authenticating the user will be redirected to the first of these that exists:\n\n- the page they tried to view before they were redirected to the login page;\n- a route named `after_login`, if any;\n- your root route.\n\n\n### Logout\n\nSend a DELETE request to `quo_vadis.logout_path`.  For example:\n\n```ruby\nbutton_to 'Log out', quo_vadis.logout_path, method: :delete\n```\n\nNote you are responsible for removing any application session data you want removed.  To do so, subclass `QuoVadis::SessionsController` and override the `destroy` method:\n\n```ruby\n# app/controllers/custom_sessions_controller.rb\nclass CustomSessionsController \u003c QuoVadis::SessionsController\n  def destroy\n    reset_session\n    super\n  end\nend\n```\n\nAdd a route:\n\n```ruby\n# config/routes.rb\ndelete 'logout', to: 'custom_sessions#destroy'\n```\n\nAnd then point your log out button at your custom action:\n\n```ruby\nbutton_to 'Log out', main_app.logout_path, method: :delete\n```\n\n\n### Two-factor authentication (2FA) or Two-step verification (2SV)\n\nIf you do not want 2FA at all, set `QuoVadis.two_factor_authentication_mandatory false` in your configuration and skip the rest of this section.\n\nIf you do want 2FA, you can choose whether it is mandatory or optional for your users by setting `QuoVadis.two_factor_authentication_mandatory \u003ctrue|false\u003e` in your configuration.\n\nUse `before_action :require_two_factor_authentication` in your controllers (which supersedes `:require_password_authentication`).  This will require the user, after authenticating with their password, to authenticate with 2FA – when 2FA is mandatory, or when it is optional and the user has set up 2FA.\n\nHere's the workflow for a user setting up optional 2FA:\n\n1. User visits their 2FA overview page.\n2. [2FA overview page] User clicks a link to set up 2FA (TOTP for now).\n3. [TOTP setup page] User scans the QR code with their authenticator and enters the 6-digit one-time password.\n4. QuoVadis verifies the one-time password, generates 5 backup recovery codes, and redirects the user to the recovery codes page (or back to step 3 if the OTP is invalid).\n5. [Recovery code page] User views and hopefully saves their 5 recovery codes.\n\nWhen 2FA is mandatory the workflow starts automatically at step 3 after password authentication.\n\nIn your views, have a link where users can manage their 2FA:\n\n```ruby\nlink_to '2FA', quo_vadis.twofa_path\n```\n\nWrite the 2FA overview page ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/twofas/show.html.erb)).  It must be in `app/views/quo_vadis/twofas/show.html.:format`.  This page allows the user to set up 2FA, deactivate or reset it, and generate new recovery codes.\n\nNext, write the TOTP setup page ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/totps/new.html.erb)).  It must be in `app/views/quo_vadis/totps/new.html.:format`.  This page shows the user a QR code (and the key as text) which they scan with their authenticator.\n\nNext, write the recovery codes page ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/recovery_codes/index.html.erb)).  It must be in `app/views/quo_vadis/recovery_codes/index.html.:format`.  This shows the recovery codes immediately after TOTP is setup, and immediately after generating fresh recovery codes, but not otherwise.\n\nNext, write the TOTP challenge page where a user inputs their 6-digit TOTP ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/totps/challenge.html.erb)).  It must be in `app/views/quo_vadis/totps/challenge.html.:format`.  It's a good idea to link to the recovery code page (`challenge_recovery_codes_path`) for any user who has lost their authenticator.\n\nFinally, write the recovery code challenge page where a user inputs one of their recovery codes ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/recovery_codes/challenge.html.erb)).  It must be in `app/views/quo_vadis/recovery_codes/challenge.html.:format`.  A recovery code can only be used once, and using one deactivates TOTP – so the user will have to set it up again next time.\n\n\n### Change password\n\nTo change their password, the user must provide their current one as well as the new one.\n\nWrite the change-password form ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/passwords/edit.html.erb)).  It must be in `app/views/quo_vadis/passwords/edit.html.:format`.\n\nAfter the password has been changed, the user is redirected to the first of:\n\n- your route named `:after_password_change`, if any;\n- your root route.\n\nA successful password change logs out any other sessions the user has (e.g. on other devices).\n\n\n### Reset password\n\nThe user can reset their password if they lose it and cannot log in.  The flow is:\n\n1. [Request password-reset page] User enters their identifier (not their email unless the identifier is email).\n2. QuoVadis emails the user a 6-digit reset code, which is valid for `QuoVadis.password_reset_otp_lifetime`, and redirects to the password-reset page.\n3. [The email] The user reads the code.\n4. [Password-reset page] The user enters the 6-digt code and their new password and clicks the save button.\n5. QuoVadis sets the user's password and logs them in.\n\nFirst, write the page where the user requests a password-reset ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/password_resets/new.html.erb)).  It must be in `app/views/quo_vadis/password_resets/new.html.:format`.  It must POST the user's identifier (not email, unless the identifier is email) to `password_reset_path`.\n\nNow write the email view ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/reset_password.text.erb)).  It must be in `app/views/quo_vadis/mailer/reset_password.{text,html}.erb` and output the `@otp` variable.  See the [Configuration](#configuration) section for how to set QuoVadis's emails' from addresses, headers, etc.\n\nNow write the page where the user types in the reset code from the email and their new password ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/password_resets/edit.html.erb)).  It must be in `app/views/quo_vadis/password_resets/edit.html.:format` and must PUT the `otp`, `password`, and `password_confirmation` fields to `password_reset_path`.\n\nAfter the user has reset their password, they will be logged in and redirected to the first of these that exists:\n\n- a route named `:after_login`;\n- your root route.\n\nWhen the user resets their password, they are logged out of any other sessions they may have, for example on other devices.\n\n\n### Sessions\n\nA logged-in session lasts for either the browser session or `QuoVadis.session_lifetime`.  As well as having a lifetime, a session will also expire after it has been inactive for `QuoVadis.session_idle_timeout`.\n\nA user can view their active sessions and log out of any of them.\n\nWrite the view showing the sessions ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/sessions/index.html.erb)).  It must be in `app/views/quo_vadis/sessions/index.html.:format`.\n\n\n### Audit trail\n\nAn audit trail is kept of authentication events.  You can see the full list in the [`Log`](https://github.com/airblade/quo_vadis/blob/master/app/models/quo_vadis/log.rb) class.\n\nWrite the view showing the events ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/logs/index.html.erb)).  It must be in `app/views/quo_vadis/logs/index.html.:format`.\n\n\n### Notifications\n\nQuoVadis notifies users by email whenever their authentication details are changed or something suspicious happens.\n\nWrite the corresponding mailer views:\n\n- change of email ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/email_change_notification.text.erb))\n- change of identifier (unless the identifier is email) ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/identifier_change_notification.text.erb))\n- change of password ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/password_change_notification.text.erb))\n- reset of password ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/password_reset_notification.text.erb))\n- TOTP setup ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/totp_setup_notification.text.erb))\n- TOTP code used a second time ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/totp_reuse_notification.text.erb))\n- 2FA deactivated ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb))\n- recovery codes generated ([example](https://github.com/airblade/quo_vadis/blob/master/app/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb))\n\nThey must be in `app/views/quo_vadis/mailer/NAME.{text,html}.erb`.\n\n\n### Revocation\n\nYou can revoke a user's access by calling `#revoke_authentication_credentials` on the model instance.  This deletes the user's password, TOTP credential, recovery codes, and active sessions.  Their authentication logs, or audit trail, are preserved.\n\n\n## Shortcut logging in for functional, integration, and system tests\n\nInstead of going through your login page to log in before every test, you can tell QuoVadis which model to authenticate as when visiting the first URL in your test.\n\nUse a `login` param pointing to your model's global ID.  Note that the model must be able to log in normally, i.e. it must have a password (and therefore a `qv_account`).\n\nFor example:\n\n```ruby\n@user = User.create(email: '...', password: '...')\nvisit dashboard_path(login: @user.to_global_id)\n```\n\nThis only works in the test environment.\n\n\n## Configuration\n\nThis is QuoVadis' [default configuration](https://github.com/airblade/quo_vadis/blob/master/lib/quo_vadis/defaults.rb):\n\n```ruby\nQuoVadis.configure do\n  password_minimum_length               12\n  mask_ips                              false\n  cookie_name                           (Rails.env.production? ? '__Host-qv' : 'qv')\n  session_lifetime                      :session\n  session_lifetime_extend_to_end_of_day false\n  session_idle_timeout                  :lifetime\n  password_reset_otp_lifetime           10.minutes\n  accounts_require_confirmation         false\n  account_confirmation_otp_lifetime     10.minutes\n  mail_headers                          ({ from: 'Example App \u003csupport@example.com\u003e' })\n  enqueue_transactional_emails          true\n  app_name                              Rails.app_class.to_s.deconstantize  # for the TOTP QR code\n  two_factor_authentication_mandatory   true\n  mount_point                           '/'\nend\n```\n\nYou can override any of it with a similarly structured file in `config/initializers/quo_vadis.rb`.\n\nHere are the options in detail:\n\n#### `password_minimum_length` (integer)\n\nThe minimum number of characters for a password.\n\n#### `mask_ips` (boolean)\n\nWhether to mask the IP address in the sessions list and the audit trail.\n\nMasking means setting the last octet (IPv4) or the last 80 bits (IPv6) to 0.\n\n#### `cookie_name` (string)\n\nThe name of the cookie QuoVadis uses to store the session identifier.  The `__Host-` prefix is [recommended](https://developer.mozilla.org/en-US/docs/Web/API/document/cookie) in an SSL environment (but cannot be used in a non-SSL environment).\n\n#### `session_lifetime` (`:session` | `ActiveSupport::Duration` | integer)\n\nThe lifetime of a logged-in session.  Use `:session` for the browser session, or a `Duration` or number of seconds.\n\n#### `session_lifetime_extend_to_end_of_day` (boolean)\n\nWhether to extend the session's lifetime to the end of the day it will expire on.\n\nSet `true` to reduce the chance of a user being logged out while actively using your application.\n\n#### `session_idle_timeout` (`:lifetime` | `ActiveSupport::Duration` | integer)\n\nThe logged-in session is expired if the user isn't seen for this `Duration` or number of seconds.  Use `:lifetime` to set the idle timeout to the session's lifetime (i.e. to turn off the idle timeout).\n\n#### `password_reset_otp_lifetime` (`ActiveSupport::Duration` | integer)\n\nThe `Duration` or number of seconds for which a password-reset code is valid.\n\n#### `accounts_require_confirmation` (boolean)\n\nWhether new users must confirm their account before they can log in.\n\n#### `account_confirmation_otp_lifetime` (`ActiveSupport::Duration` | integer)\n\nThe `Duration` or number of seconds for which an account-confirmation code is valid.\n\n#### `mailer_superclass` (string)\n\nThe class from which QuoVadis's mailer inherits.\n\n#### `mail_headers` (hash)\n\nMail headers which QuoVadis' emails should have.\n\n#### `enqueue_transactional_emails` (boolean)\n\nSet `true` if account-confirmation and password-reset emails should be queued for later delivery (`#deliver_later`) or `false` if they should be sent inline (`#deliver_now`).\n\n#### `app_name` (string)\n\nUsed in the provisioning URI for the TOTP QR code.\n\n#### `two_factor_authentication_mandatory` (boolean)\n\nWhether users must set up and use a second authentication factor.\n\n#### `mount_point` (string)\n\nThe path prefix for QuoVadis's routes.\n\nFor example, the default login path is at `/login`.  If you set `mount_point` to `/auth`, the login path would be `/auth/login`.\n\n### Rails configuration\n\n#### Mailer URLs\n\nYou must also configure the mailer host so URLs are generated correctly in emails:\n\n```ruby\nconfig.action_mailer.default_url_options: { host: 'example.com' }\n```\n\n#### Layouts\n\nYou can specify QuoVadis's controllers' layouts in a `#to_prepare` block in your application configuration.  For example:\n\n```ruby\n# config/application.rb\nmodule YourApp\n  class Application \u003c Rails::Application\n    config.to_prepare do\n      QuoVadis::ConfirmationsController.layout 'your_layout'\n    end\n  end\nend\n```\n\n#### Routes\n\nYou can set up your post-signup, post-authentication, and post-password-change routes.  If you don't, you must have a root route.  For example:\n\n```ruby\n# config/routes.rb\nget '/signups/confirmed', to: 'dashboards#show', as: 'after_signup'\nget '/dashboard',         to: 'dashboards#show', as: 'after_login'\nget '/profile',           to: 'profiles#show',   as: 'after_password_change'\n```\n\n### I18n\n\nAll QuoVadis' text (flash messages, mail subjects, and log messages) is set via [i18n](https://github.com/airblade/quo_vadis/blob/master/config/locales/quo_vadis.en.yml).\n\nYou can override any of the messages with your own locale file at `config/locales/quo_vadis.en.yml`.\n\nIf you don't want a specific flash message at all, give the key an empty value in your locale file.\n\n\n## Intellectual Property\n\nCopyright Andrew Stewart (boss@airbladesoftware.com).\n\nReleased under the MIT licence.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fairblade%2Fquo_vadis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fairblade%2Fquo_vadis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fairblade%2Fquo_vadis/lists"}