├── spec ├── all.rb ├── words ├── sql │ ├── mssql_teardown.sql │ ├── mysql_teardown.sql │ ├── mysql_setup.sql │ └── mssql_setup.sql ├── override-views │ ├── button.str │ └── password-field.str ├── migrate │ └── 002_account_password_hash_column.rb ├── views │ ├── layout-other.str │ ├── layout.str │ ├── layout-auth-check.str │ └── login.str ├── path_class_methods_spec.rb ├── change_password_notify_spec.rb ├── reset_password_notify_spec.rb ├── otp_modify_email_spec.rb └── lib_spec.rb ├── .gitignore ├── templates ├── login.str ├── multi-phase-login.str ├── otp-setup-email.str ├── password-changed-email.str ├── otp-disabled-email.str ├── otp-unlocked-email.str ├── reset-password-notify-email.str ├── button.str ├── two-factor-auth.str ├── email-auth.str ├── add-recovery-codes.str ├── sms-request.str ├── logout.str ├── sms-auth.str ├── verify-login-change.str ├── verify-account-email.str ├── webauthn-authenticator-added-email.str ├── login-form-footer.str ├── sms-confirm.str ├── webauthn-authenticator-removed-email.str ├── login-display.str ├── email-auth-email.str ├── confirm-password.str ├── otp-auth.str ├── unlock-account-email.str ├── reset-password-email.str ├── login-field.str ├── sms-disable.str ├── password-field.str ├── password-confirm-field.str ├── close-account.str ├── otp-disable.str ├── two-factor-disable.str ├── login-confirm-field.str ├── global-logout-field.str ├── email-auth-request-form.str ├── reset-password.str ├── unlock-account.str ├── login-form.str ├── sms-code-field.str ├── otp-auth-code-field.str ├── otp-unlock-not-available.str ├── change-login.str ├── recovery-codes.str ├── unlock-account-request.str ├── verify-account.str ├── otp-unlock-failed-email.str ├── verify-login-change-email.str ├── verify-account-resend.str ├── otp-locked-out-email.str ├── recovery-auth.str ├── reset-password-request.str ├── create-account.str ├── otp-unlock.str ├── sms-setup.str ├── change-password.str ├── two-factor-manage.str ├── webauthn-autofill.str ├── webauthn-auth.str ├── webauthn-setup.str ├── otp-setup.str ├── webauthn-remove.str └── remember.str ├── www ├── public │ ├── images │ │ ├── rodauth-db-diagram.png │ │ ├── rodauth-db-diagram-thumb.png │ │ └── rodauth.svg │ └── css │ │ └── rodauth.css ├── make_www.rb ├── pages │ ├── index.erb │ └── development.erb └── layout.erb ├── demo-site ├── config.ru └── views │ ├── layout.erb │ └── index.erb ├── lib ├── roda │ └── plugins │ │ └── rodauth.rb └── rodauth │ ├── features │ ├── reset_password_notify.rb │ ├── change_password_notify.rb │ ├── otp_modify_email.rb │ ├── webauthn_modify_email.rb │ ├── update_password_hash.rb │ ├── logout.rb │ ├── path_class_methods.rb │ ├── otp_lockout_email.rb │ ├── password_pepper.rb │ ├── webauthn_verify_account.rb │ ├── disallow_common_passwords.rb │ ├── password_grace_period.rb │ ├── jwt_cors.rb │ ├── session_expiration.rb │ ├── webauthn_autofill.rb │ ├── email_base.rb │ └── http_basic_auth.rb │ └── version.rb ├── doc ├── release_notes │ ├── 2.42.0.txt │ ├── 1.7.0.txt │ ├── 2.17.0.txt │ ├── 2.25.0.txt │ ├── 1.1.0.txt │ ├── 1.22.0.txt │ ├── 2.20.0.txt │ ├── 2.12.0.txt │ ├── 1.4.0.txt │ ├── 1.21.0.txt │ ├── 2.24.0.txt │ ├── 2.30.0.txt │ ├── 2.28.0.txt │ ├── 2.14.0.txt │ ├── 2.23.0.txt │ ├── 2.40.0.txt │ ├── 1.9.0.txt │ ├── 1.8.0.txt │ ├── 2.16.0.txt │ ├── 1.2.0.txt │ ├── 2.13.0.txt │ ├── 2.33.0.txt │ ├── 2.39.0.txt │ ├── 2.9.0.txt │ ├── 1.14.0.txt │ ├── 2.35.0.txt │ ├── 1.15.0.txt │ ├── 1.17.0.txt │ ├── 1.3.0.txt │ ├── 2.5.0.txt │ ├── 2.8.0.txt │ ├── 2.4.0.txt │ ├── 1.18.0.txt │ ├── 2.29.0.txt │ ├── 1.16.0.txt │ ├── 2.18.0.txt │ ├── 1.6.0.txt │ ├── 2.21.0.txt │ ├── 2.1.0.txt │ ├── 2.11.0.txt │ ├── 1.11.0.txt │ ├── 2.7.0.txt │ ├── 1.23.0.txt │ ├── 2.36.0.txt │ ├── 2.27.0.txt │ ├── 1.13.0.txt │ ├── 2.6.0.txt │ ├── 2.22.0.txt │ ├── 2.3.0.txt │ ├── 2.34.0.txt │ ├── 2.2.0.txt │ ├── 2.31.0.txt │ ├── 2.38.0.txt │ ├── 2.41.0.txt │ ├── 2.26.0.txt │ ├── 2.15.0.txt │ └── 2.10.0.txt ├── guides │ ├── query_params.rdoc │ ├── already_authenticated.rdoc │ ├── links.rdoc │ ├── totp_or_recovery.rdoc │ ├── redirects.rdoc │ ├── email_only.rdoc │ ├── case_insensitive_login.rdoc │ ├── migrate_password_hash_algorithm.rdoc │ ├── reset_password_autologin.rdoc │ ├── password_column.rdoc │ ├── change_table_and_column_names.rdoc │ ├── require_mfa.rdoc │ ├── render_confirmation.rdoc │ ├── delay_password.rdoc │ ├── email_requirements.rdoc │ ├── i18n.rdoc │ ├── share_configuration.rdoc │ ├── status_column.rdoc │ ├── create_account_programmatically.rdoc │ ├── password_confirmation.rdoc │ ├── login_return.rdoc │ ├── paths.rdoc │ ├── password_requirements.rdoc │ ├── alternative_login.rdoc │ └── admin_activation.rdoc ├── update_password_hash.rdoc ├── path_class_methods.rdoc ├── webauthn_verify_account.rdoc ├── change_password_notify.rdoc ├── reset_password_notify.rdoc ├── http_basic_auth.rdoc ├── webauthn_autofill.rdoc ├── logout.rdoc ├── webauthn_login.rdoc ├── password_grace_period.rdoc ├── otp_modify_email.rdoc ├── verify_account_grace_period.rdoc ├── email_base.rdoc ├── disallow_common_passwords.rdoc ├── webauthn_modify_email.rdoc ├── change_login.rdoc ├── session_expiration.rdoc ├── close_account.rdoc ├── change_password.rdoc ├── disallow_password_reuse.rdoc ├── jwt_cors.rdoc ├── create_account.rdoc └── otp_lockout_email.rdoc ├── SECURITY.md ├── MIT-LICENSE ├── javascript ├── webauthn_autofill.js ├── webauthn_setup.js └── webauthn_auth.js └── .github └── workflows └── ci.yml /spec/all.rb: -------------------------------------------------------------------------------- 1 | Dir['./spec/*_spec.rb'].each{|f| require f} 2 | -------------------------------------------------------------------------------- /spec/words: -------------------------------------------------------------------------------- 1 | password 2 | least 3 | little 4 | lasts 5 | nibble 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /rodauth-*.gem 2 | /rdoc 3 | /coverage 4 | /www/public/*.html 5 | /www/public/rdoc/ 6 | -------------------------------------------------------------------------------- /templates/login.str: -------------------------------------------------------------------------------- 1 | #{rodauth.login_form_header} 2 | #{rodauth.render('login-form')} 3 | #{rodauth.login_form_footer} 4 | -------------------------------------------------------------------------------- /www/public/images/rodauth-db-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/rodauth/HEAD/www/public/images/rodauth-db-diagram.png -------------------------------------------------------------------------------- /spec/sql/mssql_teardown.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE rodauth_test; 2 | DROP LOGIN rodauth_test; 3 | DROP LOGIN rodauth_test_password; 4 | GO 5 | exit 6 | -------------------------------------------------------------------------------- /templates/multi-phase-login.str: -------------------------------------------------------------------------------- 1 | #{rodauth.login_form_header} 2 | #{rodauth.render_multi_phase_login_forms} 3 | #{rodauth.login_form_footer} 4 | -------------------------------------------------------------------------------- /templates/otp-setup-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has setup TOTP authentication for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /templates/password-changed-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has changed the password for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /templates/otp-disabled-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has disabled TOTP authentication for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /templates/otp-unlocked-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has unlocked TOTP authentication for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /templates/reset-password-notify-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has reset the password for the account 2 | associated to this email address. 3 | -------------------------------------------------------------------------------- /www/public/images/rodauth-db-diagram-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyevans/rodauth/HEAD/www/public/images/rodauth-db-diagram-thumb.png -------------------------------------------------------------------------------- /spec/sql/mysql_teardown.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE rodauth_test; 2 | DROP USER 'rodauth_test'@'localhost'; 3 | DROP USER 'rodauth_test_password'@'localhost'; 4 | -------------------------------------------------------------------------------- /demo-site/config.ru: -------------------------------------------------------------------------------- 1 | $:.unshift(::File.expand_path('../../lib', __FILE__)) 2 | require ::File.expand_path('../rodauth_demo', __FILE__) 3 | run RodauthDemo::App.app 4 | -------------------------------------------------------------------------------- /lib/roda/plugins/rodauth.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require_relative '../../rodauth' 4 | 5 | Roda::RodaPlugins.register_plugin(:rodauth, Rodauth) 6 | -------------------------------------------------------------------------------- /templates/button.str: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /spec/override-views/button.str: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /templates/two-factor-auth.str: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /templates/email-auth.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.email_auth_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.button(rodauth.login_button)} 5 |
6 | -------------------------------------------------------------------------------- /templates/add-recovery-codes.str: -------------------------------------------------------------------------------- 1 |
#{rodauth.recovery_codes.map{|s| h s}.join("\n\n")}
2 | #{"#{rodauth.add_recovery_codes_heading}#{rodauth.render('recovery-codes')}" if rodauth.can_add_recovery_codes?} 3 | -------------------------------------------------------------------------------- /templates/sms-request.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.sms_request_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.button(rodauth.sms_request_button)} 5 |
6 | -------------------------------------------------------------------------------- /templates/logout.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.logout_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.button(rodauth.logout_button, :class=>'btn btn-warning')} 5 |
6 | -------------------------------------------------------------------------------- /doc/release_notes/2.42.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * Rodauth now avoids mixing string and symbol keys in a hash used to 4 | create a JWT in the jwt_refresh feature. This avoids warnings in 5 | the current json gem, and will avoid errors in json gem version 3+. 6 | -------------------------------------------------------------------------------- /templates/sms-auth.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.sms_auth_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('sms-code-field')} 5 | #{rodauth.button(rodauth.sms_auth_button)} 6 |
7 | -------------------------------------------------------------------------------- /templates/verify-login-change.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.verify_login_change_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.button(rodauth.verify_login_change_button)} 5 |
6 | -------------------------------------------------------------------------------- /templates/verify-account-email.str: -------------------------------------------------------------------------------- 1 | Someone has created an account with this email address. If you did not create 2 | this account, please ignore this message. If you created this account, please go to 3 | #{rodauth.verify_account_email_link} 4 | to verify the account. 5 | -------------------------------------------------------------------------------- /templates/webauthn-authenticator-added-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has added a WebAuthn authenticator to the 2 | account associated to this email address. There are now #{rodauth.account_webauthn_ids.length} WebAuthn 3 | authenticator(s) with access to the account. 4 | -------------------------------------------------------------------------------- /templates/login-form-footer.str: -------------------------------------------------------------------------------- 1 | #{rodauth.login_form_footer_links_heading} 2 | 7 | -------------------------------------------------------------------------------- /templates/sms-confirm.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.sms_confirm_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('sms-code-field')} 5 | #{rodauth.button(rodauth.sms_confirm_button)} 6 |
7 | -------------------------------------------------------------------------------- /templates/webauthn-authenticator-removed-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) has removed a WebAuthn authenticator from the 2 | account associated to this email address. There are now #{rodauth.account_webauthn_ids.length} WebAuthn 3 | authenticator(s) with access to the account. 4 | -------------------------------------------------------------------------------- /templates/login-display.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.login_hidden_field} 3 | 4 |
#{h rodauth.param(rodauth.login_param)}
5 |
6 | -------------------------------------------------------------------------------- /templates/email-auth-email.str: -------------------------------------------------------------------------------- 1 | Someone has requested a login link for the account with this email 2 | address. If you did not request a login link, please ignore this 3 | message. If you requested a login link, please go to 4 | #{rodauth.email_auth_email_link} 5 | to login to this account. 6 | -------------------------------------------------------------------------------- /spec/sql/mysql_setup.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'rodauth_test'@'localhost' IDENTIFIED BY 'rodauth_test'; 2 | CREATE USER 'rodauth_test_password'@'localhost' IDENTIFIED BY 'rodauth_test'; 3 | CREATE DATABASE rodauth_test; 4 | GRANT ALL ON rodauth_test.* TO 'rodauth_test_password'@'localhost' WITH GRANT OPTION; 5 | -------------------------------------------------------------------------------- /templates/confirm-password.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.confirm_password_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field')} 5 | #{rodauth.button(rodauth.confirm_password_button)} 6 |
7 | -------------------------------------------------------------------------------- /templates/otp-auth.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.otp_auth_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('otp-auth-code-field')} 5 | #{rodauth.button(rodauth.otp_auth_button)} 6 |
7 | 8 | #{rodauth.otp_auth_form_footer} 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Potential security issues can be publicly reported in the same 6 | manner as non-security issues (e.g. on GitHub Issues). However, 7 | if you would like to report them privately, you can report them 8 | via email to code@jeremyevans.net. 9 | -------------------------------------------------------------------------------- /spec/migrate/002_account_password_hash_column.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | up do 3 | # Only for testing of account_password_hash_column, not recommended for new 4 | # applications 5 | add_column :accounts, :ph, String 6 | end 7 | 8 | down do 9 | drop_column :accounts, :ph 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/views/layout-other.str: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Foo #{@title} 5 | 6 | 7 | #{"
#{flash['error2']}
" if flash['error2']} 8 | #{"
#{flash['notice2']}
" if flash['notice2']} 9 | #{yield} 10 | 11 | 12 | -------------------------------------------------------------------------------- /templates/unlock-account-email.str: -------------------------------------------------------------------------------- 1 | Someone has requested that the account with this email be unlocked. 2 | If you did not request the unlocking of this account, please ignore this 3 | message. If you requested the unlocking of this account, please go to 4 | #{rodauth.unlock_account_email_link} 5 | to unlock this account. 6 | -------------------------------------------------------------------------------- /templates/reset-password-email.str: -------------------------------------------------------------------------------- 1 | Someone has requested a password reset for the account with this email 2 | address. If you did not request a password reset, please ignore this 3 | message. If you requested a password reset, please go to 4 | #{rodauth.reset_password_email_link} 5 | to reset the password for the account. 6 | -------------------------------------------------------------------------------- /templates/login-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 | #{rodauth.input_field_string(rodauth.login_param, 'login', :type=>rodauth.login_input_type, :autocomplete=>rodauth.login_field_autocomplete_value)} 4 |
5 | -------------------------------------------------------------------------------- /templates/sms-disable.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.sms_disable_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 5 | #{rodauth.button(rodauth.sms_disable_button)} 6 |
7 | -------------------------------------------------------------------------------- /doc/release_notes/1.7.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The reset password, unlock account, and verify account features now 4 | temporarily store the feature-specific keys in the session instead 5 | of keeping them as parameters, which avoids leaking the keys to 6 | asset hosts or other external servers via the HTTP Referer header. 7 | -------------------------------------------------------------------------------- /templates/password-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 | #{rodauth.input_field_string(rodauth.password_param, 'password', :type => 'password', :autocomplete=>rodauth.password_field_autocomplete_value)} 4 |
5 | -------------------------------------------------------------------------------- /www/public/images/rodauth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rodauth 4 | 5 | -------------------------------------------------------------------------------- /templates/password-confirm-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 | #{rodauth.input_field_string(rodauth.password_confirm_param, 'password-confirm', :type => 'password', :autocomplete=>'new-password')} 4 |
5 | -------------------------------------------------------------------------------- /spec/override-views/password-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 | #{rodauth.input_field_string(rodauth.password_param, 'password', :type => 'password', :autocomplete=>rodauth.password_field_autocomplete_value)} 4 |
5 | -------------------------------------------------------------------------------- /templates/close-account.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.close_account_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.close_account_requires_password?} 5 | #{rodauth.button(rodauth.close_account_button, :class=>'btn btn-danger')} 6 |
7 | -------------------------------------------------------------------------------- /templates/otp-disable.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.otp_disable_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 5 | #{rodauth.button(rodauth.otp_disable_button, :class=>'btn btn-warning')} 6 |
7 | -------------------------------------------------------------------------------- /templates/two-factor-disable.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.two_factor_disable_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 5 | #{rodauth.button(rodauth.two_factor_disable_button)} 6 |
7 | -------------------------------------------------------------------------------- /doc/guides/query_params.rdoc: -------------------------------------------------------------------------------- 1 | = Pass query parameters to auth URLs 2 | 3 | The *_path and *_url methods allow passing additional query parameters: 4 | 5 | rodauth.create_account_path(type: "seller") 6 | #=> "/create-account?type=seller" 7 | 8 | rodauth.login_url(type: "operator") 9 | #=> "https//example.com/login?type=operator" 10 | -------------------------------------------------------------------------------- /templates/login-confirm-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 | #{rodauth.input_field_string(rodauth.login_confirm_param, 'login-confirm', :type=>rodauth.login_input_type, :autocomplete=>rodauth.login_uses_email? ? "email" : "on")} 4 |
5 | -------------------------------------------------------------------------------- /templates/global-logout-field.str: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /templates/email-auth-request-form.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.email_auth_request_additional_form_tags} 3 | #{rodauth.csrf_tag(rodauth.email_auth_request_path)} 4 | #{rodauth.login_hidden_field} 5 | #{rodauth.button(rodauth.email_auth_request_button)} 6 |
7 | -------------------------------------------------------------------------------- /templates/reset-password.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.reset_password_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field')} 5 | #{rodauth.render('password-confirm-field') if rodauth.require_password_confirmation?} 6 | #{rodauth.button(rodauth.reset_password_button)} 7 |
8 | -------------------------------------------------------------------------------- /templates/unlock-account.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.unlock_account_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.unlock_account_explanatory_text} 5 | #{rodauth.render('password-field') if rodauth.unlock_account_requires_password?} 6 | #{rodauth.button(rodauth.unlock_account_button)} 7 |
8 | -------------------------------------------------------------------------------- /spec/sql/mssql_setup.sql: -------------------------------------------------------------------------------- 1 | CREATE LOGIN rodauth_test WITH PASSWORD = 'Rodauth1.'; 2 | CREATE LOGIN rodauth_test_password WITH PASSWORD = 'Rodauth1.'; 3 | CREATE DATABASE rodauth_test; 4 | GO 5 | USE rodauth_test; 6 | GO 7 | CREATE USER rodauth_test FOR LOGIN rodauth_test; 8 | GRANT CONNECT, EXECUTE TO rodauth_test; 9 | EXECUTE sp_changedbowner 'rodauth_test_password'; 10 | GO 11 | exit 12 | -------------------------------------------------------------------------------- /doc/release_notes/2.17.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The jwt_refresh feature now works for unverified accounts when using 4 | the verify_account_grace_period feature. 5 | 6 | * When trying to create an account that already exists but is 7 | unverified, Rodauth now returns a 4xx response. 8 | 9 | * When trying to login to an unverified account, Rodauth now returns a 10 | 4xx response. 11 | -------------------------------------------------------------------------------- /templates/login-form.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.login_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.skip_login_field_on_login? ? rodauth.render('login-display') : rodauth.render('login-field')} 5 | #{rodauth.render('password-field') unless rodauth.skip_password_field_on_login?} 6 | #{rodauth.button(rodauth.login_button)} 7 |
8 | -------------------------------------------------------------------------------- /templates/sms-code-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | #{rodauth.input_field_string(rodauth.sms_code_param, 'sms-code', :value => '', :autocomplete=>'one-time-code', :inputmode=>'numeric')} 6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /doc/release_notes/2.25.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * You can now disable routing to specific routes by calling the 4 | related *_route configuration method with nil or false. The main 5 | reason you would want to do this is if you want to load a feature, 6 | but only want to use it for internal requests (using the 7 | internal_request feature), and not have the feature's routes exposed 8 | to users. 9 | -------------------------------------------------------------------------------- /templates/otp-auth-code-field.str: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | #{rodauth.input_field_string(rodauth.otp_auth_param, 'otp-auth-code', :value=>'', :autocomplete=>"off", :inputmode=>'numeric')} 6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /doc/release_notes/1.1.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The rodauth plugin now supports :csrf=>false and :flash=>false 4 | options. This will make it so it no longer depends on the csrf 5 | or flash plugins, which is useful when the csrf and flash 6 | functionality is provided via a different approach, such as 7 | when rodauth is being used inside middleware in a Rails 8 | application with the roda-rails library. 9 | -------------------------------------------------------------------------------- /doc/update_password_hash.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Update Password Hash Feature 2 | 3 | The update password hash feature updates the hash for the password whenever 4 | the hash cost changes. For example, if you have a cost of 8, and later 5 | increase the cost to 10, anytime the user authenticates correctly with 6 | their password, their password hash will change from one that uses a cost 7 | of 8 to one that uses a cost of 10. 8 | -------------------------------------------------------------------------------- /templates/otp-unlock-not-available.str: -------------------------------------------------------------------------------- 1 |

#{rodauth.otp_unlock_consecutive_successes_label}: #{rodauth.otp_unlock_num_successes}

2 |

#{rodauth.otp_unlock_required_consecutive_successes_label}: #{rodauth.otp_unlock_auths_required}

3 |

#{rodauth.otp_unlock_next_auth_attempt_label}: #{rodauth.otp_unlock_next_auth_attempt_after.strftime(rodauth.strftime_format)}

4 |

#{rodauth.otp_unlock_next_auth_attempt_refresh_label}

5 | -------------------------------------------------------------------------------- /templates/change-login.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.change_login_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('login-field')} 5 | #{rodauth.render('login-confirm-field') if rodauth.require_login_confirmation?} 6 | #{rodauth.render('password-field') if rodauth.change_login_requires_password?} 7 | #{rodauth.button(rodauth.change_login_button)} 8 |
9 | -------------------------------------------------------------------------------- /templates/recovery-codes.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.recovery_codes_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 5 | #{rodauth.button(rodauth.recovery_codes_button || rodauth.view_recovery_codes_button, :name=>(rodauth.add_recovery_codes_param if rodauth.recovery_codes_button))} 6 |
7 | -------------------------------------------------------------------------------- /templates/unlock-account-request.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.unlock_account_request_additional_form_tags} 3 | #{rodauth.csrf_tag(rodauth.unlock_account_request_path)} 4 | #{rodauth.login_hidden_field} 5 | #{rodauth.unlock_account_request_explanatory_text} 6 | #{rodauth.button(rodauth.unlock_account_request_button)} 7 |
8 | -------------------------------------------------------------------------------- /templates/verify-account.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.verify_account_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.verify_account_set_password?} 5 | #{rodauth.render('password-confirm-field') if rodauth.verify_account_set_password? && rodauth.require_password_confirmation?} 6 | #{rodauth.button(rodauth.verify_account_button)} 7 |
8 | -------------------------------------------------------------------------------- /templates/otp-unlock-failed-email.str: -------------------------------------------------------------------------------- 1 | Someone (hopefully you) attempted to unlock TOTP authentication for the 2 | account associated to this email address, but failed as the 3 | authentication code submitted was not correct. 4 | 5 | If you did not initiate the TOTP authentication failure that generated 6 | this email, that means someone already has partial access to your 7 | account, but is unable to use TOTP authentication to fully authenticate 8 | themselves. 9 | -------------------------------------------------------------------------------- /templates/verify-login-change-email.str: -------------------------------------------------------------------------------- 1 | Someone with an account has requested their login be changed to this email address: 2 | 3 | Old Login: #{rodauth.verify_login_change_old_login} 4 | 5 | New Login: #{rodauth.verify_login_change_new_login} 6 | 7 | If you did not request this login change, please ignore this message. If you 8 | requested this login change, please go to 9 | #{rodauth.verify_login_change_email_link} 10 | to verify the login change. 11 | -------------------------------------------------------------------------------- /doc/release_notes/1.22.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A jwt_cors feature has been added, handling Cross-Origin Resource 4 | Sharing when using the jwt feature, including supporting CORS 5 | preflight requests. 6 | 7 | = Other Improvements 8 | 9 | * Mail templates that include links (e.g. for verifying accounts), 10 | now add a space after the link and before the newline, fixing 11 | issues with some web mail providers that have broken auto-linkers. 12 | -------------------------------------------------------------------------------- /doc/guides/already_authenticated.rdoc: -------------------------------------------------------------------------------- 1 | = Skip login page if already authenticated 2 | 3 | In some cases it may be useful to skip login/registration pages when the user 4 | is already logged in. This can be achieved as follows. Note that this only 5 | matters if the user manually navigates to the login or create account pages. 6 | 7 | plugin :rodauth do 8 | # Redirect logged in users to the wherever login redirects to 9 | already_logged_in { redirect login_redirect } 10 | end 11 | -------------------------------------------------------------------------------- /www/make_www.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'erb' 3 | require './lib/rodauth/version' 4 | Dir.chdir(File.dirname(__FILE__)) 5 | erb = ERB.new(File.read('layout.erb')) 6 | Dir['pages/*.erb'].each do |page| 7 | public_loc = "#{page.gsub(/\Apages\//, 'public/').sub('.erb', '.html')}" 8 | content = content = ERB.new(File.read(page)).result(binding) 9 | title = title = File.basename(page.sub('.erb', '')) 10 | File.open(public_loc, 'wb'){|f| f.write(erb.result(binding))} 11 | end 12 | -------------------------------------------------------------------------------- /lib/rodauth/features/reset_password_notify.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:reset_password_notify, :ResetPasswordNotify) do 5 | depends :reset_password 6 | loaded_templates %w'reset-password-notify-email' 7 | email :reset_password_notify, 'Password Reset Completed', :translatable=>true 8 | 9 | private 10 | 11 | def after_reset_password 12 | super 13 | send_reset_password_notify_email 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/rodauth/features/change_password_notify.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:change_password_notify, :ChangePasswordNotify) do 5 | depends :change_password, :email_base 6 | loaded_templates %w'password-changed-email' 7 | email :password_changed, 'Password Changed', :translatable=>true 8 | 9 | private 10 | 11 | def after_change_password 12 | super 13 | send_password_changed_email 14 | end 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /spec/views/layout.str: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #{@title} 5 | 6 | 7 | #{"
#{opts[:sessions_convert_symbols] ? flash['error'] : flash[:error]}
" if opts[:sessions_convert_symbols] ? flash['error'] : flash[:error]} 8 | #{"
#{opts[:sessions_convert_symbols] ? flash['notice'] : flash[:notice]}
" if opts[:sessions_convert_symbols] ? flash['notice'] : flash[:notice]} 9 | #{yield} 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/release_notes/2.20.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * When using the active_sessions and remember features together, 4 | doing a global logout will automatically remove the remember key for 5 | the account, so the account will no longer be able to automatically 6 | create new sessions using the remember key. 7 | 8 | * The default value of webauthn_rp_id now removes the port from the 9 | origin if it exists, since the WebAuthn spec does not allow ports 10 | in the relying party identifier. 11 | -------------------------------------------------------------------------------- /templates/verify-account-resend.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.verify_account_resend_additional_form_tags} 3 | #{rodauth.csrf_tag(rodauth.verify_account_resend_path)} 4 | #{rodauth.verify_account_resend_explanatory_text} 5 | #{rodauth.param_or_nil(rodauth.login_param) ? rodauth.login_hidden_field : rodauth.render('login-field')} 6 | #{rodauth.button(rodauth.verify_account_resend_button)} 7 |
8 | -------------------------------------------------------------------------------- /doc/guides/links.rdoc: -------------------------------------------------------------------------------- 1 | = Display authentication links 2 | 3 | You can retrieve a relative URL to any Rodauth action by calling the 4 | corresponding *_path method on the Rodauth instance: 5 | 6 | Sign in 7 | Sign up 8 | 9 | For absolute URLs instead of paths, you can use the *_url methods: 10 | 11 | Sign in 12 | Sign up 13 | -------------------------------------------------------------------------------- /templates/otp-locked-out-email.str: -------------------------------------------------------------------------------- 1 | TOTP authentication has been locked out on your account due to too many 2 | consecutive authentication failures. You can attempt to unlock TOTP 3 | authentication for your account by consecutively authenticating via 4 | TOTP multiple times. 5 | 6 | If you did not initiate the TOTP authentication failures that 7 | caused TOTP authentication to be locked out, that means someone already 8 | has partial access to your account, but is unable to use TOTP 9 | authentication to fully authenticate themselves. 10 | -------------------------------------------------------------------------------- /templates/recovery-auth.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.recovery_auth_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 |
5 | 6 | #{rodauth.input_field_string(rodauth.recovery_codes_param, 'recovery-code', :value => '', :autocomplete=>'off')} 7 |
8 | #{rodauth.button(rodauth.recovery_auth_button)} 9 |
10 | -------------------------------------------------------------------------------- /doc/path_class_methods.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Path Class Methods Feature 2 | 3 | The path class methods feature allows for calling the *_path and *_url 4 | methods directly on the class, as opposed to an instance of the class. 5 | 6 | In order for the *_url methods to be used, you must use the base_url 7 | configuration so that determining the base URL doesn't depend on the 8 | submitted request, as the request will not be set when using the 9 | class method. Failure to do this will probably result in a NoMethodError 10 | being raised. 11 | -------------------------------------------------------------------------------- /doc/release_notes/2.12.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The following configuration methods have been added to the 4 | active_sessions feature: 5 | 6 | * active_sessions_insert_hash 7 | * active_sessions_key 8 | * active_sessions_update_hash 9 | * update_current_session? 10 | 11 | These methods allow you to control what gets inserted and 12 | updated into the active_sessions_table, and to control 13 | whether to perform updates. 14 | 15 | = Other Improvements 16 | 17 | * A typo was fixed in the default unlock account email. 18 | -------------------------------------------------------------------------------- /doc/webauthn_verify_account.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for WebAuthn Verify Account Feature 2 | 3 | The webauthn_verify_account feature implements setting up an WebAuthn authenticator 4 | during the account verification process, and making such setup 5 | a requirement for account verification. By default, it disables 6 | asking for a password during account creation and verification, 7 | allowing for completely passwordless designs, where the only 8 | authentication option is WebAuthn. It depends on the verify_account 9 | and webauthn features. 10 | -------------------------------------------------------------------------------- /doc/release_notes/1.4.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A update_password_hash feature has been added, which will update 4 | the password hash for the account whenever the account's current 5 | password hash has a cost different from the currently configured 6 | password hash cost. 7 | 8 | This allows you to increase the password hash cost for all 9 | accounts or for certain types of accounts, and have the password 10 | hashes automatically updated to use the new cost the next time the 11 | correct password is provided for the account. 12 | -------------------------------------------------------------------------------- /templates/reset-password-request.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.reset_password_request_additional_form_tags} 3 | #{rodauth.csrf_tag(rodauth.reset_password_request_path)} 4 | #{rodauth.reset_password_explanatory_text} 5 | #{rodauth.param_or_nil(rodauth.login_param) && !rodauth.field_error(rodauth.login_param) ? rodauth.login_hidden_field : rodauth.render('login-field')} 6 | #{rodauth.button(rodauth.reset_password_request_button)} 7 |
8 | -------------------------------------------------------------------------------- /templates/create-account.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.create_account_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('login-field')} 5 | #{rodauth.render('login-confirm-field') if rodauth.require_login_confirmation?} 6 | #{rodauth.render('password-field') if rodauth.create_account_set_password?} 7 | #{rodauth.render('password-confirm-field') if rodauth.create_account_set_password? && rodauth.require_password_confirmation?} 8 | #{rodauth.button(rodauth.create_account_button)} 9 |
10 | -------------------------------------------------------------------------------- /doc/change_password_notify.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Change Password Notify Feature 2 | 3 | The change password notify feature emails the user when their password 4 | is changed using the change password feature. 5 | 6 | == Auth Value Methods 7 | 8 | password_changed_email_body :: Body to use for the password changed emails 9 | password_changed_email_subject :: Subject to use for the password changed emails 10 | 11 | == Auth Methods 12 | 13 | create_password_changed_email :: A Mail::Message for the password changed email to send. 14 | send_password_changed_email :: Send the password changed email. 15 | -------------------------------------------------------------------------------- /doc/release_notes/1.21.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * rotp 5.1 is now supported in the otp feature. Previous rotp 4 | versions down to rotp 2.1.1 remain supported. 5 | 6 | * When using the otp feature without the sms or recovery_codes 7 | features, if an account gets locked out from OTP authentication due 8 | to multiple invalid OTP authentication codes, automatically log 9 | them out, and redirect them to the login page. Previously, the 10 | default behavior in this case could be a redirect loop if OTP 11 | authentication is required for the user on the default_redirect 12 | page. 13 | -------------------------------------------------------------------------------- /doc/guides/totp_or_recovery.rdoc: -------------------------------------------------------------------------------- 1 | = Allow recovery code on TOTP code field 2 | 3 | If using the otp feature, for convenience you might want to allow 4 | the user to enter the recovery code into the TOTP code field, instead 5 | of requiring they use the separate recovery codes form. You can 6 | implement this using the following configuration: 7 | 8 | plugin :rodauth do 9 | enable :login, :logout, :otp, :recovery_codes 10 | 11 | before_otp_auth_route do 12 | if recovery_code_match?(param(otp_auth_param)) 13 | two_factor_authenticate("recovery_code") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /doc/guides/redirects.rdoc: -------------------------------------------------------------------------------- 1 | = Change redirect destination 2 | 3 | You can change the redirect destination for any Rodauth action by overriding 4 | the corresponding *_redirect method: 5 | 6 | plugin :rodauth do 7 | enable :login, :logout, :create_account, :reset_password 8 | 9 | # Redirect to "/dashboard" after login 10 | login_redirect "/dashboard" 11 | 12 | # Redirect to wherever login redirects to after creating account 13 | create_account_redirect { login_redirect } 14 | 15 | # Redirect to login page after password reset 16 | reset_password_redirect { login_path } 17 | end 18 | -------------------------------------------------------------------------------- /spec/views/layout-auth-check.str: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #{@title} 5 | 6 | 7 | #{"
#{opts[:sessions_convert_symbols] ? flash['error'] : flash[:error]}
" if opts[:sessions_convert_symbols] ? flash['error'] : flash[:error]} 8 | #{"
#{opts[:sessions_convert_symbols] ? flash['notice'] : flash[:notice]}
" if opts[:sessions_convert_symbols] ? flash['notice'] : flash[:notice]} 9 | Is #{'Not ' unless rodauth.logged_in?}Logged In 10 | Is #{'Not ' unless rodauth.authenticated?}Authenticated 11 | #{yield} 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/guides/email_only.rdoc: -------------------------------------------------------------------------------- 1 | = Allow only email authentication 2 | 3 | When using the email authentication feature, you can avoid other authentication 4 | mechanisms entirely as follows: 5 | 6 | plugin :rodauth do 7 | enable :login, :email_auth, :create_account, :verify_account 8 | 9 | create_account_set_password? false 10 | verify_account_set_password? false 11 | force_email_auth? true 12 | end 13 | 14 | With this configuration, users won't be required to enter a password on 15 | registration, and on login the email authentication link will automatically be 16 | sent after the email address is entered. 17 | -------------------------------------------------------------------------------- /doc/release_notes/2.24.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * rodauth.otp_available? has been added for checking whether the 4 | account is allowed to authenticate with OTP. It returns true 5 | when the account has setup OTP and OTP use is not locked out. 6 | 7 | * rodauth.recovery_codes_available? has been added for checking 8 | whether the account is allowed to authenticate using a recovery 9 | code. It returns true when there are any available recovery 10 | codes for the account to use. 11 | 12 | = Other Improvements 13 | 14 | * The otp feature no longer includes the tag for svg images, 15 | since that results in invalid HTML. 16 | -------------------------------------------------------------------------------- /lib/rodauth/features/otp_modify_email.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:otp_modify_email, :OtpModifyEmail) do 5 | depends :otp, :email_base 6 | 7 | loaded_templates %w'otp-setup-email otp-disabled-email' 8 | email :otp_setup, 'TOTP Authentication Setup', :translatable=>true 9 | email :otp_disabled, 'TOTP Authentication Disabled', :translatable=>true 10 | 11 | private 12 | 13 | def after_otp_setup 14 | super 15 | send_otp_setup_email 16 | end 17 | 18 | def after_otp_disable 19 | super 20 | send_otp_disabled_email 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /templates/otp-unlock.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.otp_unlock_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 |

#{rodauth.otp_unlock_consecutive_successes_label}: #{rodauth.otp_unlock_num_successes}

5 |

#{rodauth.otp_unlock_required_consecutive_successes_label}: #{rodauth.otp_unlock_auths_required}

6 |

#{rodauth.otp_unlock_next_auth_deadline_label}: #{rodauth.otp_unlock_deadline.strftime(rodauth.strftime_format)}

7 | #{rodauth.render('otp-auth-code-field')} 8 | #{rodauth.button(rodauth.otp_unlock_button)} 9 |
10 | 11 | #{rodauth.otp_unlock_form_footer} 12 | -------------------------------------------------------------------------------- /doc/release_notes/2.30.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A webauthn_autofill feature has been added to allow autofilling 4 | webauthn credentials during login (also known as conditional 5 | mediation). This allows for easier login using passkeys. 6 | This requires a supported browser and operating system on the 7 | client side to work. 8 | 9 | = Other Improvements 10 | 11 | * The load_memory method in the remember feature no longer raises 12 | a NoMethodError if the there is a remember cookie, the session is 13 | already logged in, and the account no longer exists. The 14 | load_memory method now removes the remember cookie and clears the 15 | session in that case. 16 | -------------------------------------------------------------------------------- /doc/release_notes/2.28.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A webauthn_key_insert_hash configuration method has been added when 4 | using the webauthn feature, making it easier to add new columns to 5 | the webauthn key data, such as a custom name for the authenticator. 6 | 7 | = Other Improvements 8 | 9 | * When using the verify_account_grace_period feature, logged_in? now 10 | returns false for sessions where the grace period has expired. 11 | 12 | * When using the internal_request and reset_password features, 13 | submitting an internal request for an invalid login no longer tries 14 | to render a reset password request form. 15 | 16 | * The password_hash method is now public. 17 | -------------------------------------------------------------------------------- /doc/release_notes/2.14.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A remembered_session_id method has been added for getting the 4 | account id from a valid remember token, without modifying the 5 | session to log the account in. 6 | 7 | = Other Improvements 8 | 9 | * The jwt_refresh feature's support for allowing refresh with 10 | an expired access token now works even if the Rodauth 11 | configuration uses an incorrect prefix. 12 | 13 | * The internal account_in_unverified_grace_period? method now 14 | returns false if an account has not been loaded and the 15 | session has not been logged in. Previously, calling this 16 | method in such cases would result in an exception being 17 | raised. 18 | -------------------------------------------------------------------------------- /doc/guides/case_insensitive_login.rdoc: -------------------------------------------------------------------------------- 1 | = Case insensitive logins 2 | 3 | If your database schema doesn't support case insensitive logins, you can tell 4 | Rodauth to automatically lowercase login param values during authentication and 5 | persistence via the +normalize_login+ configuration option: 6 | 7 | normalize_login(&:downcase) 8 | 9 | Of the four database types Rodauth officially supports (PostgreSQL, MySQL, 10 | Microsoft SQL Server, and SQLite), only SQLite does not support a case 11 | insensitive column for storing logins by default. However, other databases could 12 | be configured to not use a case insensitive column for logins by default, in 13 | which case you would want to use this setting. 14 | -------------------------------------------------------------------------------- /doc/release_notes/2.23.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * The otp feature now uses the :use_path option when rendering QR 4 | codes, resulting in significantly smaller svg images. 5 | 6 | * Removing all multifactor authentication methods now removes the fact 7 | that the session was authenticated via SMS, if the user used SMS as 8 | an authentication method for the current session. 9 | 10 | * The invalid domain check in the internal_request feature now works 11 | correctly when using the rack master branch. 12 | 13 | * The :httponly cookie option is no longer set automatically in the 14 | remember feature if the :http_only cookie option was provided by the 15 | user (rack recognizes both options). 16 | -------------------------------------------------------------------------------- /doc/release_notes/2.40.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A reset_password_request_for_unverified_account configuration method 4 | is now available. This allows you to configure the behavior if an 5 | unverified account requests a password reset. If the method is not 6 | used, the default behavior remains to show an error for the login 7 | parameter. 8 | 9 | = Other Improvements 10 | 11 | * In the otp_unlock feature, instead of using a meta refresh tag in 12 | the HTML, a refresh HTTP header is used. This should fix the page 13 | not automatically refreshing in some browsers. You can customize 14 | this behavior by using the 15 | otp_unlock_not_available_set_refresh_header configuration method. 16 | -------------------------------------------------------------------------------- /doc/guides/migrate_password_hash_algorithm.rdoc: -------------------------------------------------------------------------------- 1 | = Migrate users passwords from bcrypt to argon2 or back 2 | 3 | If you are currently using the default bcrypt password hash algorithm, and want to 4 | gradually migrate to the argon2 password hash algorithm, you can use both the argon2 5 | and update_password_hash features: 6 | 7 | plugin :rodauth do 8 | enable :login, :update_password_hash, :argon2 9 | end 10 | 11 | When a user with a current bcrypt password hash next successfully uses their 12 | password, their password hash will be migrated to argon2. 13 | 14 | If for some reason you want to migrate back from argon2 to bcrypt, you can set 15 | use_argon2? false in your Rodauth configuration. 16 | -------------------------------------------------------------------------------- /templates/sms-setup.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.sms_setup_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 5 |
6 | 7 |
8 |
9 | #{rodauth.input_field_string(rodauth.sms_phone_param, 'sms-phone', :type=>rodauth.sms_phone_input_type, :autocomplete=>'tel')} 10 |
11 |
12 |
13 | #{rodauth.button(rodauth.sms_setup_button)} 14 |
15 | -------------------------------------------------------------------------------- /doc/release_notes/1.9.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * Roda.precompile_rodauth_templates has been added. This method 4 | allows for precompiling the templates that rodauth uses, which 5 | allows for memory saving when using a forking webserver that 6 | preloads the application, and also allows Rodauth to be used 7 | with an application that uses chroot after loading. 8 | 9 | = Improvements 10 | 11 | * If requesting a password reset link more than once, the same 12 | password reset key will be used. Previously, subsequent 13 | emails after the first request would contain an invalid key, 14 | so if the email for the original request was lost, you could 15 | not generate another key until that key expired. 16 | -------------------------------------------------------------------------------- /templates/change-password.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.change_password_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.change_password_requires_password?} 5 |
6 | 7 | #{rodauth.input_field_string(rodauth.new_password_param, 'new-password', :type => 'password', :autocomplete=>"new-password")} 8 |
9 | #{rodauth.render('password-confirm-field') if rodauth.require_password_confirmation?} 10 | #{rodauth.button(rodauth.change_password_button)} 11 |
12 | -------------------------------------------------------------------------------- /doc/release_notes/1.8.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * When using a browser, Rodauth now uses an appropriate 401, 403, 4 | or 422 error status for errors instead of using 200 success status. 5 | Many configuration methods have been added to customize the status 6 | codes used for specific types of errors. 7 | 8 | * The json_response_custom_error_status? configuration method 9 | has been added to the jwt feature, which if set to true makes 10 | the jwt feature use the same error status codes for JSON API 11 | requests that it would use for browser requests. For backward 12 | compatibility, the default is to continue to use the 400 13 | error status for all errors in the JSON API, but this will 14 | change in Rodauth 2. 15 | -------------------------------------------------------------------------------- /doc/guides/reset_password_autologin.rdoc: -------------------------------------------------------------------------------- 1 | = Autologin after password reset 2 | 3 | When the user resets their password, by default they are not automatically 4 | logged in. You can change this behaviour and login the user automatically 5 | after password reset. 6 | 7 | plugin :rodauth do 8 | enable :login, :logout, :reset_password 9 | 10 | reset_password_autologin? true 11 | end 12 | 13 | Similarly, when the verify login change feature is used, the user is not 14 | automatically logged in after verifying the login change. You can configure 15 | Rodauth to automatically log the user in in this case: 16 | 17 | plugin :rodauth do 18 | enable :login, :logout, :verify_login_change 19 | 20 | verify_login_change_autologin? true 21 | end 22 | -------------------------------------------------------------------------------- /lib/rodauth/version.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | # The major version of Rodauth, updated only for major changes that are 5 | # likely to require modification to apps using Rodauth. 6 | MAJOR = 2 7 | 8 | # The minor version of Rodauth, updated for new feature releases of Rodauth. 9 | MINOR = 42 10 | 11 | # The patch version of Rodauth, updated only for bug fixes from the last 12 | # feature release. 13 | TINY = 0 14 | 15 | # The full version of Rodauth as a string 16 | VERSION = "#{MAJOR}.#{MINOR}.#{TINY}".freeze 17 | 18 | # The full version of Rodauth as a number (1.17.0 => 11700) 19 | VERSION_NUMBER = MAJOR*10000 + MINOR*100 + TINY 20 | 21 | def self.version 22 | VERSION 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /doc/release_notes/2.16.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * Rodauth.lib has been added for using Rodauth purely as a library, 4 | useful in non-web applications: 5 | 6 | require 'rodauth' 7 | rodauth = Rodauth.lib do 8 | enable :create_account, :change_password 9 | end 10 | rodauth.create_account(login: 'foo@example.com', password: '...') 11 | rodauth.change_password(account_id: 24601, password: '...') 12 | 13 | This is built on top of the internal_request feature, and works by 14 | creating a Roda application with the rodauth plugin, and returning 15 | the related Rodauth::Auth class. 16 | 17 | = Other Improvements 18 | 19 | * The internal_request feature now works correctly for configurations 20 | where only_json? is set to true. 21 | -------------------------------------------------------------------------------- /templates/two-factor-manage.str: -------------------------------------------------------------------------------- 1 | #{rodauth.two_factor_setup_heading unless rodauth.two_factor_setup_links.empty?} 2 | 3 | 8 | 9 | #{rodauth.two_factor_remove_heading unless rodauth.two_factor_remove_links.empty?} 10 | 11 | 17 | -------------------------------------------------------------------------------- /lib/rodauth/features/webauthn_modify_email.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:webauthn_modify_email, :WebauthnModifyEmail) do 5 | depends :webauthn, :email_base 6 | 7 | loaded_templates %w'webauthn-authenticator-added-email webauthn-authenticator-removed-email' 8 | email :webauthn_authenticator_added, 'WebAuthn Authenticator Added', :translatable=>true 9 | email :webauthn_authenticator_removed, 'WebAuthn Authenticator Removed', :translatable=>true 10 | 11 | private 12 | 13 | def after_webauthn_setup 14 | super 15 | send_webauthn_authenticator_added_email 16 | end 17 | 18 | def after_webauthn_remove 19 | super 20 | send_webauthn_authenticator_removed_email 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rodauth/features/update_password_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:update_password_hash, :UpdatePasswordHash) do 5 | depends :login_password_requirements_base 6 | 7 | def password_match?(password) 8 | if (result = super) && update_password_hash? 9 | @update_password_hash = false 10 | set_password(password) 11 | end 12 | 13 | result 14 | end 15 | 16 | private 17 | 18 | def update_password_hash? 19 | password_hash_cost != @current_password_hash_cost || @update_password_hash 20 | end 21 | 22 | def get_password_hash 23 | if hash = super 24 | @current_password_hash_cost = extract_password_hash_cost(hash) 25 | end 26 | 27 | hash 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rodauth/features/logout.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:logout, :Logout) do 5 | notice_flash "You have been logged out" 6 | loaded_templates %w'logout' 7 | view 'logout', 'Logout' 8 | additional_form_tags 9 | before 10 | after 11 | button 'Logout' 12 | redirect{require_login_redirect} 13 | response 14 | 15 | auth_methods :logout 16 | 17 | route do |r| 18 | before_logout_route 19 | 20 | r.get do 21 | logout_view 22 | end 23 | 24 | r.post do 25 | transaction do 26 | before_logout 27 | logout 28 | after_logout 29 | end 30 | logout_response 31 | end 32 | end 33 | 34 | def logout 35 | clear_session 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/rodauth/features/path_class_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:path_class_methods, :PathClassMethods) do 5 | def post_configure 6 | super 7 | 8 | klass = self.class 9 | klass.features.each do |feature_name| 10 | feature = FEATURES[feature_name] 11 | feature.routes.each do |handle_meth| 12 | route = handle_meth.to_s.sub(/\Ahandle_/, '') 13 | path_meth = :"#{route}_path" 14 | url_meth = :"#{route}_url" 15 | instance = klass.allocate.freeze 16 | klass.define_singleton_method(path_meth){|opts={}| instance.send(path_meth, opts)} 17 | klass.define_singleton_method(url_meth){|opts={}| instance.send(url_meth, opts)} 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /doc/reset_password_notify.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Reset Password Notify Feature 2 | 3 | The reset password notify feature emails the user after the user has 4 | reset their password. The user has already been sent a reset password 5 | email by this point, so they know a password reset was requested, but 6 | this feature allows for confirming that the password reset process 7 | was completed. Depends on the reset_password feature. 8 | 9 | == Auth Value Methods 10 | 11 | reset_password_notify_email_subject :: The subject to use for the reset password notify email. 12 | reset_password_notify_email_body :: The body to use for the reset password notify email. 13 | 14 | == Auth Methods 15 | 16 | create_reset_password_notify_email :: A Mail::Message for the reset password notify email. 17 | send_reset_password_notify_email :: Send the reset password notify email. 18 | -------------------------------------------------------------------------------- /doc/release_notes/1.2.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An otp_drift configuration method has been added to the otp plugin, 4 | which allows you to set the number of seconds of allowed drift. This 5 | makes the otp plugin easier to use if the client and server do not 6 | have good synchronize to the same time source. 7 | 8 | = Other Improvements 9 | 10 | * Passwords containing the ASCII NUL character "\0" are no longer 11 | allowed, as bcrypt truncates the password at the first NUL 12 | character. 13 | 14 | Note that bcrypt only uses the first 72 characters of the password 15 | when constructing the hash, but Rodauth does not enforce a limit 16 | of 72 characters. If you want to enforce a maximum password length 17 | in your application, use the password_meets_requirements? 18 | configuration method with a block and call super inside the block. 19 | -------------------------------------------------------------------------------- /templates/webauthn-autofill.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.webauthn_auth_additional_form_tags} 3 | #{rodauth.csrf_tag(rodauth.webauthn_login_path)} 4 | 5 | 6 | 7 | #{rodauth.button(rodauth.webauthn_auth_button, class: "d-none")} 8 |
9 | 10 | -------------------------------------------------------------------------------- /doc/release_notes/2.13.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A set_error_reason configuration method has been added. This method 4 | is called whenever a error occurs in Rodauth, with a symbol 5 | describing the error. The default implementation of this method does 6 | nothing, it has been added to make it easier for Rodauth users to 7 | implement custom handling for specific error types. See the Rodauth 8 | documentation for this method to see the list of symbols this method 9 | can be called with. 10 | 11 | = Other Improvements 12 | 13 | * When using active_sessions and jwt_refresh together, and allowing for 14 | expired JWTs when refreshing, you can now call 15 | rodauth.check_active_session before r.rodauth. Previously, this 16 | did not work, and you had to call rodauth.check_active_session 17 | after r.rodauth. 18 | 19 | * The default templates now also support Bootstrap 5. 20 | -------------------------------------------------------------------------------- /doc/release_notes/2.33.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * Rodauth no longer accidentally confirms an SMS number upon valid 4 | authentication by an alternative second factor. 5 | 6 | * Rodauth now automatically expires SMS confirmation codes after 24 7 | hours by default. You can use the sms_confirm_deadline 8 | configuration method to adjust the deadline. Previously, if an 9 | invalid SMS number was submitted, or the SMS confirm code was never 10 | received, it was not possible to continue SMS setup without 11 | administrative intervention. 12 | 13 | * Rodauth no longer overwrites existing primary key values when 14 | inserting new accounts. This fixes cases such as setting account 15 | primary key values to UUIDs before inserting. 16 | 17 | * When submitting a request to a valid endpoint with a missing token, 18 | Rodauth now returns an error response instead of a 404 response. 19 | -------------------------------------------------------------------------------- /doc/release_notes/2.39.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * Rodauth now supports Roda's plain_hash_response_headers plugin on 4 | Rack 3+, by using lowercase response header keys, instead of 5 | relying on Roda's default conversion of response header keys to 6 | lowercase. 7 | 8 | * When setting login_return_to_requested_location? to true, by 9 | default, Rodauth will no longer return to the requested location if 10 | it is more than 2048 bytes in size. This is to avoid exceeding the 11 | 4K cookie size limit. You can modify this limit using the new 12 | login_return_to_requested_location_max_path_size configuration 13 | method. 14 | 15 | * Rodauth now uses JSON.generate instead of JSON.fast_generate to 16 | avoid a deprecation warning in recent json gem versions. 17 | 18 | * Rodauth now uses allowed_origins instead of origin when using 19 | WebAuthn 3.4+ to avoid a deprecation warning. 20 | -------------------------------------------------------------------------------- /doc/release_notes/2.9.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A json feature has been extracted from the existing jwt feature. 4 | This feature allows for the same JSON API previously supported 5 | by the JWT feature, but stores the session information in the 6 | Rack session instead of in a separate JWT. This makes it 7 | significantly easier to have certain pages use the JSON API, 8 | and other pages the HTML forms. 9 | 10 | = Other Improvements 11 | 12 | * If the remember cookie is created in an SSL request, the Secure 13 | flag is added by default, so the cookie will not be transmitted 14 | in non-SSL requests. 15 | 16 | = Backwards Compatibility 17 | 18 | * Rodauth configurations that use the remember feature and support 19 | requests over both http and https and want to have the remember 20 | cookie transmitted over both should now include :secure=>false in 21 | remember_cookie_options. 22 | -------------------------------------------------------------------------------- /doc/http_basic_auth.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for HTTP Basic Auth Feature 2 | 3 | The HTTP basic auth feature allows logins using HTTP basic authentication, 4 | described in RFC 1945. 5 | 6 | In your routing block, you can require HTTP basic authentication via: 7 | 8 | rodauth.require_http_basic_auth 9 | 10 | If you want to allow HTTP basic authentication but not require it, you can 11 | call: 12 | 13 | rodauth.http_basic_auth 14 | 15 | == Auth Value Methods 16 | 17 | http_basic_auth_realm :: The realm to return in the WWW-Authenticate header. 18 | require_http_basic_auth? :: If true, when +rodauth.require_login+ or +rodauth.require_authentication+ is used, return a 401 status page if basic auth has not been provided, instead of redirecting to the login page. If false, +rodauth.require_login+ or +rodauth.require_authentication+ will check for HTTP basic authentication if not already logged in. False by default. 19 | -------------------------------------------------------------------------------- /templates/webauthn-auth.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.webauthn_auth_additional_form_tags} 3 | #{rodauth.csrf_tag(rodauth.webauthn_auth_form_path)} 4 | 5 | 6 | 7 |
8 | #{rodauth.button(rodauth.webauthn_auth_button)} 9 |
10 |
11 | 12 | -------------------------------------------------------------------------------- /doc/guides/password_column.rdoc: -------------------------------------------------------------------------------- 1 | = Store password hash in accounts table 2 | 3 | By default, Rodauth stores the password hash in a separate 4 | +account_password_hashes+ table. This makes it a lot less likely that the 5 | password hashes will be leaked, especially if you use Rodauth's default 6 | approach of using database functions for checking the hashes. 7 | 8 | However, if you have reasons for storing the password hashes in +accounts+ 9 | table that outweigh the security benefits of Rodauth's default approach, 10 | Rodauth supports that. 11 | 12 | To do this, add the password hash column to the +accounts+ table: 13 | 14 | alter_table :accounts do 15 | add_column :password_hash, String 16 | end 17 | 18 | And then tell Rodauth to use it: 19 | 20 | plugin :rodauth do 21 | enable :login, :logout 22 | 23 | # Use the password_hash column in the accounts table 24 | account_password_hash_column :password_hash 25 | end 26 | -------------------------------------------------------------------------------- /doc/release_notes/1.14.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A change_password_notify feature has been added, which emails the 4 | user when the change_password feature is used to change their 5 | password. This can alert the user when their password may have 6 | been changed without their knowledge. 7 | 8 | = Other Improvements 9 | 10 | * When using the account_expiration feature with the reset_password 11 | feature, resetting the passwords for expired accounts is no longer 12 | allowed. Note that the previous behavior isn't considered a 13 | security issue, because even after resetting their password, 14 | expired accounts could not login. 15 | 16 | * When using the account_expiration feature with the lockout feature, 17 | unlocking expired accounts is no longer allowed. Note that the 18 | previous behavior isn't considered a security issue, because even 19 | after unlocking the account, expired accounts could not login. 20 | -------------------------------------------------------------------------------- /templates/webauthn-setup.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.webauthn_setup_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | 5 | 6 | 7 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 8 |
9 | #{rodauth.button(rodauth.webauthn_setup_button)} 10 |
11 |
12 | 13 | -------------------------------------------------------------------------------- /www/public/css/rodauth.css: -------------------------------------------------------------------------------- 1 | nav.navbar.navbar-default a.navbar-brand, nav.navbar.navbar-default ul.nav > li > a { 2 | color: white; 3 | } 4 | 5 | h1.byline { 6 | margin-bottom: 30px; 7 | font-weight: bold; 8 | } 9 | 10 | a.navbar-brand { 11 | margin-right: 30px; 12 | font-size: 32px; 13 | } 14 | a.navbar-brand > img { 15 | display: inline-block; 16 | margin-top: -10px; 17 | } 18 | 19 | body #content a { 20 | color: #5E0000; 21 | font-weight: bold; 22 | text-decoration: underline; 23 | } 24 | nav.navbar { 25 | background: #2290FF; 26 | } 27 | body, p, h1, h2, h3, a { 28 | font-family: 'Open Sans', sans-serif; 29 | } 30 | body { 31 | color: #5E0000; 32 | background: #9DC6FF; 33 | } 34 | 35 | .path, .code { 36 | background: white; 37 | padding: 3px 5px; 38 | } 39 | 40 | .path, .code, pre code { 41 | font-family: 'PT Mono', monospace; 42 | font-size: 14px; 43 | } 44 | 45 | body, p, li { 46 | font-size: 16px; 47 | } 48 | -------------------------------------------------------------------------------- /doc/guides/change_table_and_column_names.rdoc: -------------------------------------------------------------------------------- 1 | = Change table and column names 2 | 3 | All tables that Rodauth uses will have a configuration method that ends with 4 | +_table+ for configuring the table name. For example, if you store user accounts 5 | in the +users+ table instead of +accounts+ table, you can use the following 6 | in your configuration: 7 | 8 | accounts_table :users 9 | 10 | All columns that Rodauth uses will have a configuration method that ends with 11 | +_column+ for configuring the column name. For example, if you are storing the 12 | login for accounts in the +login+ column instead of the +email+ column, you 13 | can use the following in your configuration: 14 | 15 | login_column :login 16 | 17 | Please see the documentation for Rodauth features for the names of the 18 | configuration methods that you can use. You can see the default values for 19 | the tables and columns in the {"Creating tables" section of the README}[rdoc-ref:README.rdoc]. 20 | -------------------------------------------------------------------------------- /doc/release_notes/2.35.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A throw_rodauth_error method has been added to make it easier 4 | for external extensions to throw the expected error value without 5 | setting a field error. 6 | 7 | = Improvements 8 | 9 | * If an account is not currently logged in, but Rodauth knows the 10 | related account id, remove_all_active_sessions and related 11 | methods in the active_sessions plugin will now remove sessions 12 | for the related account. 13 | 14 | * When using the internal_request feature and subclasses, 15 | internal_request_configuration blocks in superclasses are now 16 | respected when creating the internal request class for a 17 | subclass. When creating the internal request in the subclass, 18 | this behaves as if all internal_request_configuration blocks 19 | were specified directly in the subclass. 20 | 21 | * An ignored block warning on Ruby 3.4 is now avoided by having 22 | Rodauth.load_dependencies accept a block. 23 | -------------------------------------------------------------------------------- /doc/release_notes/1.15.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * create_account_set_password? and verify_account_set_password? 4 | configuration methods have been added to the create_account and 5 | verify_account features. Setting: 6 | 7 | verify_account_set_password? true 8 | 9 | in your rodauth configuration will change Rodauth so that instead 10 | of asking for a password on the create account form, it will ask for 11 | a password on the verify account form. 12 | 13 | This can fix a possible issue where an attacker creates an account 14 | for a user with a password the attacker knows. If the user clicks 15 | on the link in the verify account email and clicks on the button on 16 | the verify account page, the attacker would have have a verified 17 | account that they know the password to. 18 | 19 | By setting verify_account_set_password? to true, you can ensure that 20 | only the user who has access to the email can enter the password for 21 | the account. 22 | -------------------------------------------------------------------------------- /doc/release_notes/1.17.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * Support has been added for using Roda's route_csrf plugin with 4 | request-specific CSRF tokens. When loading the Rodauth into 5 | your Roda app, specify the :csrf=>:route_csrf plugin option 6 | so that Rodauth will load the route_csrf plugin instead of 7 | the csrf plugin. 8 | 9 | * The use_request_specific_csrf_tokens? configuration option 10 | has been added, it defaults to true when the the 11 | :csrf=>:route_csrf option is used when loading the plugin. 12 | 13 | * If you have custom templates for the reset password request, 14 | unlock account request, or verify account resend link 15 | request, you will have to update them to use the new 16 | request-specific CSRF token feature. 17 | 18 | = Backwards Compatibility 19 | 20 | * The csrf_tag configuration method now accepts the path as 21 | an optional argument, previously it accepted no arguments. 22 | The optional argument defaults to the path of the current 23 | request. 24 | -------------------------------------------------------------------------------- /doc/release_notes/1.3.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A login_maximum_length configuration method has been added. This 4 | defaults to 255, and rodauth will now show an error message if a 5 | user tries to create a login longer than this setting. 6 | 7 | = Backwards Compatibility 8 | 9 | * Rodauth's documentation and test code now use :Bignum instead of 10 | Bignum for database-independent 64-bit integer types. This is 11 | because using Bignum is now deprecated in Sequel as it will stop 12 | working correctly in ruby 2.4+, due to the unification of Fixnum 13 | and Bignum into Integer. 14 | 15 | Rodauth's library code does not use either :Bignum or Bignum, but if 16 | you are starting to use Rodauth and are copying the example 17 | migration from Rodauth's documentation, or you are running the 18 | migrations in Rodauth's tests, you now need to use Sequel 4.35.0+. 19 | 20 | * Some files related to the hosting of the demo site on Heroku have 21 | been removed from the repository. 22 | -------------------------------------------------------------------------------- /doc/release_notes/2.5.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A login_return_to_requested_location_path configuration method has 4 | been added to the login feature. This controls the path to redirect 5 | to if using login_return_to_requested_location?. By default, this 6 | is the same as the fullpath of the request that required login if 7 | that request was a GET request, and nil if that request was not a 8 | GET request. Previously, the fullpath of that request was used even 9 | if it was not a GET request, which caused problems as browsers use a 10 | GET request for redirects, and it is a bad idea to redirect to a path 11 | that may not handle GET requests. 12 | 13 | * A change_login_needs_verification_notice_flash configuration method 14 | has been added to the verify_login_change feature, for allowing 15 | translations when using the feature and not using the 16 | change_login_notice_flash configuration method. 17 | 18 | = Other Improvements 19 | 20 | * new_password_label is now translatable. 21 | -------------------------------------------------------------------------------- /spec/path_class_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe 'path_class_methods feature' do 4 | it "should add *_path and *_url methods as class methods" do 5 | rodauth do 6 | prefix '/foo' 7 | base_url 'https://foo.example.com' 8 | enable :path_class_methods, :login, :logout 9 | end 10 | roda do |r| 11 | end 12 | 13 | app.rodauth.login_path.must_equal '/foo/login' 14 | app.rodauth.logout_url.must_equal 'https://foo.example.com/foo/logout' 15 | 16 | app.rodauth.logout_path('bar'=>'baz').must_equal '/foo/logout?bar=baz' 17 | app.rodauth.login_url('bar'=>'baz').must_equal 'https://foo.example.com/foo/login?bar=baz' 18 | end 19 | 20 | it "*_path should work without base_url" do 21 | rodauth do 22 | enable :path_class_methods, :login, :logout 23 | end 24 | roda do |r| 25 | end 26 | 27 | app.rodauth.logout_path.must_equal '/logout' 28 | 29 | proc{app.rodauth.login_url}.must_raise NoMethodError 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /doc/webauthn_autofill.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for WebAuthn Autofill Feature 2 | 3 | The webauthn_autofill feature enables autofill UI (aka "conditional mediation") 4 | for WebAuthn credentials, logging the user in on selection. It depends on the 5 | webauthn_login feature. 6 | 7 | This feature allows generating WebAuthn credential options and submitting a 8 | WebAuthn login request without providing a login, which can be used 9 | independently from the autofill UI. 10 | 11 | == Auth Value Methods 12 | 13 | webauthn_autofill? :: Whether to activate the autofill UI on the login page. 14 | webauthn_autofill_js :: The javascript code to execute on the login page to enable autofill UI. 15 | webauthn_autofill_js_route :: The route to the webauthn autofill javascript file. 16 | webauthn_invalid_webauthn_id_message :: The error message to show when provided WebAuthn ID wasn't found in the database. 17 | 18 | == Auth Methods 19 | 20 | before_webauthn_autofill_js_route :: Run arbitrary code before handling a webauthn autofill javascript route. 21 | -------------------------------------------------------------------------------- /spec/change_password_notify_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe 'Rodauth change_password_notify feature' do 4 | it "should email when using change password" do 5 | rodauth do 6 | enable :login, :logout, :change_password_notify 7 | change_password_requires_password? false 8 | end 9 | roda do |r| 10 | r.rodauth 11 | r.root{view :content=>""} 12 | end 13 | 14 | login 15 | page.current_path.must_equal '/' 16 | 17 | visit '/change-password' 18 | fill_in 'New Password', :with=>'0123456' 19 | fill_in 'Confirm Password', :with=>'0123456' 20 | click_button 'Change Password' 21 | page.find('#notice_flash').text.must_equal "Your password has been changed" 22 | 23 | page.current_path.must_equal '/' 24 | email = email_sent 25 | email.subject.must_equal "Password Changed" 26 | email.body.to_s.must_equal <false option. 21 | -------------------------------------------------------------------------------- /doc/guides/render_confirmation.rdoc: -------------------------------------------------------------------------------- 1 | = Render confirmation view 2 | 3 | Most Rodauth actions redirect and display a flash notice after they're successfully performed. However, in some cases you may wish to render a view confirming that the action was successful, for nicer user experience. 4 | 5 | For example, when the user creates an account, you might render a page with a call to action to verify their account. Assuming you've created an +account_created+ view template alongside your other Rodauth templates, you can configure the following: 6 | 7 | after_create_account do 8 | # render "account_created" view template with page title of "Account created!" 9 | return_response view("account_created", "Account created!") 10 | end 11 | 12 | Similarly, when the user has requested a password reset, you can render a page telling them to check their email: 13 | 14 | after_reset_password_request do 15 | # render "password_reset_sent" view template with page title of "Password sent!" 16 | return_response view("password_reset_sent", "Password sent!") 17 | end 18 | -------------------------------------------------------------------------------- /doc/logout.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Logout Feature 2 | 3 | The logout feature implements a logout button, which clears the session. 4 | It is the simplest feature. 5 | 6 | == Auth Value Methods 7 | 8 | logout_additional_form_tags :: HTML fragment containing additional form tags to use on the logout form. 9 | logout_button :: The text to use for the logout button. 10 | logout_notice_flash :: The flash notice to show after logout. 11 | logout_page_title :: The page title to use on the logout form. 12 | logout_redirect :: Where to redirect after a logout. 13 | logout_route :: The route to the logout action. Defaults to +logout+. 14 | 15 | == Auth Methods 16 | 17 | after_logout :: Run arbitrary code after logout. 18 | before_logout :: Run arbitrary code before logout. 19 | before_logout_route :: Run arbitrary code before handling a logout route. 20 | logout :: Log the user out, by default clearing the session. 21 | logout_response :: Return a response after a successful logout. By default, redirects to +logout_redirect+. 22 | logout_view :: The HTML to use for the logout form. 23 | -------------------------------------------------------------------------------- /spec/views/login.str: -------------------------------------------------------------------------------- 1 |
2 | #{csrf_tag if respond_to?(:csrf_tag)} 3 | 4 |
5 | 6 |
7 | #{rodauth.field_error(rodauth.login_param)} 8 |
9 |
10 |
11 | 12 |
13 | #{rodauth.field_error(rodauth.password_param)} 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /templates/otp-setup.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.otp_setup_additional_form_tags} 3 | 4 | #{"" if rodauth.otp_keys_use_hmac?} 5 | #{rodauth.csrf_tag} 6 |
7 |

#{rodauth.otp_secret_label}: #{rodauth.otp_user_key}

8 |

#{rodauth.otp_provisioning_uri_label}: #{rodauth.otp_provisioning_uri}

9 |
10 | 11 |
12 |
13 |
14 |

#{rodauth.otp_qr_code}

15 |
16 |
17 | 18 |
19 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 20 | #{rodauth.render('otp-auth-code-field')} 21 | #{rodauth.button(rodauth.otp_setup_button)} 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /doc/webauthn_login.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for WebAuthn Login Feature 2 | 3 | The webauthn_login feature implements passwordless authentication via 4 | WebAuthn. It depends on the login and webauthn features. 5 | 6 | == Auth Value Methods 7 | 8 | webauthn_login_user_verification_additional_factor? :: Whether passwordless login via WebAuthn should consider user verification as 2nd factor when using multifactor authentication, false by default. Setting this to true means that the app trusts the user verification done by the authenticator is strong enough to be considered an additional factor. 9 | webauthn_login_error_flash :: The flash error to show if there is a failure during passwordless login via WebAuthn. 10 | webauthn_login_failure_redirect :: Whether to redirect if there is a failure during passwordless login via WebAuthn. 11 | webauthn_login_route :: The route to the webauthn login action. 12 | 13 | == Auth Methods 14 | 15 | before_webauthn_login :: Any actions to take before passwordless login via WebAuthn. 16 | before_webauthn_login_route :: Run arbitrary code before handling a webauthn login route. 17 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2023 Jeremy Evans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /doc/guides/delay_password.rdoc: -------------------------------------------------------------------------------- 1 | = Set password when verifying account 2 | 3 | If you want to request less information from the user on registration, you can 4 | ask the user to set their password only when they verify their account: 5 | 6 | plugin :rodauth do 7 | enable :login, :logout, :verify_account 8 | verify_account_set_password? true 9 | end 10 | 11 | Note that this is already the default behaviour when verify account feature is 12 | loaded, but it's not when verify account grace period is used, because it would 13 | prevent the account from logging in during the grace period. You can work around 14 | this by automatically remembering their login during account creation using the 15 | remember feature. Be aware that remembering accounts has effects beyond the 16 | verification period, and this would only allow automatic logins from the browser 17 | that created the account. 18 | 19 | plugin :rodauth do 20 | enable :login, :logout, :verify_account_grace_period, :remember 21 | verify_account_set_password? true 22 | after_create_account do 23 | remember_login 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /doc/guides/email_requirements.rdoc: -------------------------------------------------------------------------------- 1 | = Customize email requirements 2 | 3 | By default, Rodauth requires emails to have at least 3 characters and at most 4 | 255 bytes. You can modify the minimum and maximum length: 5 | 6 | plugin :rodauth do 7 | enable :login, :logout, :create_account 8 | 9 | # Require emails to have at least 5 characters 10 | login_minimum_length 5 11 | 12 | # Don't allow emails longer than 100 characters 13 | login_maximum_length 100 14 | 15 | # Don't allow emails larger than 200 bytes 16 | login_maximum_bytes 200 17 | end 18 | 19 | You can also override email address validation, and do more advanced email 20 | checks, such as checking whether the email address exists using the 21 | {Truemail}[https://github.com/truemail-rb/truemail] gem: 22 | 23 | require "truemail" 24 | 25 | Truemail.configure do |config| 26 | config.verifier_email = "verifier@example.com" 27 | end 28 | 29 | plugin :rodauth do 30 | enable :login, :logout, :create_account 31 | 32 | login_valid_email? do |email| 33 | super(email) && Truemail.valid?(email) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /templates/webauthn-remove.str: -------------------------------------------------------------------------------- 1 |
2 | #{rodauth.webauthn_remove_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 | #{rodauth.render('password-field') if rodauth.two_factor_modifications_require_password?} 5 |
6 | #{(usage = rodauth.account_webauthn_usage; last_id = usage.keys.last; usage;).map do |id, last_use| 7 | last_use = last_use.strftime(rodauth.strftime_format) if last_use.is_a?(Time) 8 | input = rodauth.input_field_string(rodauth.webauthn_remove_param, "webauthn-remove-#{h id}", :type=>'radio', :class=>"form-check-input", :skip_error_message=>true, :value=>id, :required=>false) 9 | label = "" 10 | error = rodauth.formatted_field_error(rodauth.webauthn_remove_param) if id == last_id 11 | "
#{input}#{label}#{error}
" 12 | end.join("\n")} 13 |
14 | #{rodauth.button(rodauth.webauthn_remove_button)} 15 |
16 | -------------------------------------------------------------------------------- /doc/guides/i18n.rdoc: -------------------------------------------------------------------------------- 1 | = Translate with i18n gem 2 | 3 | Rodauth allows transforming user-facing text configuration such as flash 4 | messages, validation errors, labels etc. via the +translate+ configuration 5 | method. This method receives a name of a configuration along with its default 6 | value, and is expected to return the result text. 7 | 8 | You can use this to perform translations using the 9 | {i18n gem}[https://github.com/ruby-i18n/i18n]: 10 | 11 | plugin :rodauth do 12 | enable :login, :logout, :reset_password 13 | 14 | translate do |key, default| 15 | I18n.translate("rodauth.#{key}") || default 16 | end 17 | end 18 | 19 | Your translation file may then look something like this: 20 | 21 | en: 22 | rodauth: 23 | login_notice_flash: "You have been signed in" 24 | require_login_error_flash: "Login is required for accessing this page" 25 | no_matching_login_message: "user with this email address doesn't exist" 26 | reset_password_email_subject: "Password Reset Instructions" 27 | 28 | Alternatively, you can use the 29 | {rodauth-i18n}[https://github.com/janko/rodauth-i18n] gem. 30 | -------------------------------------------------------------------------------- /doc/guides/share_configuration.rdoc: -------------------------------------------------------------------------------- 1 | = Share configuration via inheritance 2 | 3 | If you have multiple configurations that needs to share some amount of 4 | authentication behaviour, you can do so through inheritance. For example: 5 | 6 | require "rodauth" 7 | 8 | class RodauthBase < Rodauth::Auth 9 | configure do 10 | # common authentication configuration 11 | end 12 | end 13 | 14 | class RodauthMain < RodauthBase # inherit common configuration 15 | configure do 16 | # main-specific authentication configuration 17 | end 18 | end 19 | 20 | class RodauthAdmin < RodauthBase # inherit common configuration 21 | configure do 22 | # admin-specific authentication configuration 23 | end 24 | end 25 | 26 | class RodauthApp < Roda 27 | plugin :rodauth, auth_class: RodauthMain 28 | plugin :rodauth, auth_class: RodauthAdmin, name: :admin 29 | # ... 30 | end 31 | 32 | However, when doing this, you need to be careful that you do not use a 33 | configuration method in a superclass, and then load a feature in a subclass 34 | that overrides the configuration you set in the superclass. 35 | -------------------------------------------------------------------------------- /doc/release_notes/2.4.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A password_pepper feature has been added. This allows you to use a 4 | secret key (called a pepper) to append to passwords before hashing 5 | and hash checking. Using this approach, if an attacker obtains the 6 | password hash, it is unusable for cracking unless they can also 7 | get access to the pepper. 8 | 9 | The password_pepper feature also supports a list of previous peppers 10 | that can be used to implement secret rotation and to support 11 | compatibility with unpeppered passwords. 12 | 13 | Rodauth by default uses database functions for password hash 14 | checking on PostgreSQL, MySQL, and Microsoft SQL Server, which in 15 | general provides more security than a password pepper, but both 16 | approaches can be used simultaneously. 17 | 18 | * A session_key_prefix configuration method has been added for 19 | prefixing the values of all default session keys. This can be 20 | useful if you are using multiple Rodauth configurations in the same 21 | application and want to make sure the session keys for the separate 22 | configurations do not overlap. 23 | -------------------------------------------------------------------------------- /spec/reset_password_notify_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | describe 'Rodauth reset_password_notify feature' do 4 | it "should send email when password is reset" do 5 | rodauth do 6 | enable :reset_password_notify 7 | end 8 | roda do |r| 9 | r.rodauth 10 | r.root{view :content=>""} 11 | end 12 | 13 | visit '/login' 14 | login(:pass=>'01234567', :visit=>false) 15 | click_button 'Request Password Reset' 16 | page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to reset the password for your account" 17 | 18 | visit email_link(/(\/reset-password\?key=.+)$/) 19 | fill_in 'Password', :with=>'0123456' 20 | fill_in 'Confirm Password', :with=>'0123456' 21 | click_button 'Reset Password' 22 | page.find('#notice_flash').text.must_equal "Your password has been reset" 23 | 24 | email = email_sent 25 | email.subject.must_equal "Password Reset Completed" 26 | email.body.to_s.must_equal < 2 | #{rodauth.remember_additional_form_tags} 3 | #{rodauth.csrf_tag} 4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | #{rodauth.button(rodauth.remember_button)} 19 | 20 | -------------------------------------------------------------------------------- /doc/release_notes/2.29.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * When using the remember feature, by default, the remember deadline 4 | is extended while logged in, if it hasn't been extended in the last 5 | hour 6 | 7 | * An account! method has been added, which will return the hash for 8 | the account if already retrieved, or attempt to retrieve the 9 | account hash using the currently logged in session if not. 10 | Because of the ambiguity in the provenance of the returned account 11 | hash, callers should be careful when using this method. 12 | 13 | * A remove_active_session method has been added. You can call this 14 | method with a specific session id, and it will remove the related 15 | active session. 16 | 17 | * A render: false plugin option is now support, which will disable 18 | the automatic loading of the render plugin. This should only be 19 | used if you are completely replacing Rodauth's view rendering with 20 | your own. 21 | 22 | = Other Improvements 23 | 24 | * When logging in when using the active_sessions feature, if there is 25 | a current active session, it is removed before a new active session 26 | is created. This prevents some stale active sessions from remaining 27 | in the database (which would eventually be cleaned up later). 28 | -------------------------------------------------------------------------------- /doc/release_notes/1.16.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A disallow_common_passwords feature has been added. This feature 4 | by default will disallow the 10,000 most common passwords: 5 | 6 | enable :disallow_common_passwords 7 | 8 | You can supply your own file containing common passwords separated 9 | by newlines ("\n"): 10 | 11 | most_common_passwords_file '/path/to/file' 12 | 13 | You can also supply a password dictionary directly as any object 14 | that responds to include?: 15 | 16 | most_common_passwords some_password_dictionary_object 17 | 18 | The reason only the 10,000 most common passwords are used by 19 | default is larger password files would significantly bloat the 20 | size of the gem. Also, because the most common passwords are kept 21 | in memory by default for performance reasons, larger password 22 | files can bloat the memory usage of the process (the 23 | disallow_common_passwords feature should use around 500KB of 24 | memory by default). For very large password dictionaries, 25 | consider using a custom object that does not keep all common 26 | passwords in memory. 27 | 28 | = Other Improvements 29 | 30 | * Rodauth no longer uses the Rack::Request#[] method to get 31 | parameter values. This method is deprecated in Rack 2. 32 | -------------------------------------------------------------------------------- /doc/release_notes/2.18.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * When using the json and multifactor auth features, the JSON API can 4 | now access the multifactor-manage route to get lists of endpoints 5 | for setting up and disabling supported multifactor authentication 6 | methods. The JSON API can now also access the multifactor-auth 7 | route to get a list of endpoints for multifactor authentication for 8 | the currently logged in account. 9 | 10 | = Other Improvements 11 | 12 | * In the otp feature, the viewbox: true rqrcode option is now used 13 | when creating the QR code. This results in a QR code that is 14 | displayed better and is easier to style. This option only has 15 | an effect when using rqrcode 2+. 16 | 17 | * When using the :auth_class option when loading the rodauth plugin, 18 | the configuration name is set in the provided auth class, unless the 19 | auth class already has a configuration name set. 20 | 21 | * The example migration now recommends using a partial index on the 22 | email column in cases where the database supports partial indexes. 23 | Previously, it only recommended it on PostgreSQL. 24 | 25 | * The argon2 feature now works with argon2 2.1.0. Older versions of 26 | Rodauth work with both earlier and later versions of argon2, but 27 | not 2.1.0. 28 | -------------------------------------------------------------------------------- /doc/guides/create_account_programmatically.rdoc: -------------------------------------------------------------------------------- 1 | = Create an account record programmatically 2 | 3 | In some scenarios you might want to create an account records programmatically, 4 | for example in your tests. 5 | 6 | If you're storing passwords in a separate table, you can create an account 7 | records as follows: 8 | 9 | account_id = DB[:accounts].insert( 10 | email: "name@example.com", 11 | status_id: 2, # verified 12 | ) 13 | 14 | DB[:account_password_hashes].insert( 15 | id: account_id, 16 | password_hash: BCrypt::Password.create("secret").to_s, 17 | ) 18 | 19 | If the password is stored in a column in the accounts table: 20 | 21 | account_id = DB[:accounts].insert( 22 | email: "name@example.com", 23 | password_hash: BCrypt::Password.create("secret").to_s, 24 | status_id: 2, # verified 25 | ) 26 | 27 | If you are creating accounts in your tests, you probably want to use 28 | the +:cost+ option, otherwise you will have very slow tests: 29 | 30 | account_id = DB[:accounts].insert( 31 | email: "name@example.com", 32 | status_id: 2, # verified 33 | ) 34 | 35 | DB[:account_password_hashes].insert( 36 | id: account_id, 37 | password_hash: BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST).to_s, 38 | ) 39 | -------------------------------------------------------------------------------- /doc/email_base.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Email Base Feature 2 | 3 | The email base feature is automatically loaded when you use a Rodauth feature 4 | that requires sending emails. 5 | 6 | == Auth Value Methods 7 | 8 | allow_raw_email_token? :: When +hmac_secret+ is used, this allows the use of the raw token. This should only be set to true temporarily during a transition period from using raw tokens to using HMACed tokens. After the transition period, this should not be set, as setting this to true removes the security that HMACed tokens add. 9 | default_post_email_redirect :: Where to redirect after sending an email. This is the default redirect location for all redirects after an email is sent when the account is not logged in. Also includes cases where an email is not sent due to rate limiting. 10 | email_from :: The from address to use for emails sent by Rodauth. 11 | email_subject_prefix :: The prefix to use for email subjects 12 | require_mail? :: Set to false to not require mail, useful if using a different library for sending email. 13 | 14 | == Auth Methods 15 | 16 | create_email(subject, body) :: Return a Mail::Message instance with the given subject and body. 17 | email_to :: The email address to send emails to, by default the login of the current account. 18 | send_email(email) :: Deliver a given Mail::Message instance. 19 | -------------------------------------------------------------------------------- /lib/rodauth/features/otp_lockout_email.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:otp_lockout_email, :OtpLockoutEmail) do 5 | depends :otp_unlock, :email_base 6 | 7 | loaded_templates %w'otp-locked-out-email otp-unlocked-email otp-unlock-failed-email' 8 | email :otp_locked_out, 'TOTP Authentication Locked Out', :translatable=>true 9 | email :otp_unlocked, 'TOTP Authentication Unlocked', :translatable=>true 10 | email :otp_unlock_failed, 'TOTP Authentication Unlocking Failed', :translatable=>true 11 | 12 | auth_value_method :send_otp_locked_out_email?, true 13 | auth_value_method :send_otp_unlocked_email?, true 14 | auth_value_method :send_otp_unlock_failed_email?, true 15 | 16 | private 17 | 18 | def after_otp_authentication_failure 19 | super 20 | 21 | if otp_locked_out? && send_otp_locked_out_email? 22 | send_otp_locked_out_email 23 | end 24 | end 25 | 26 | def after_otp_unlock_auth_success 27 | super 28 | 29 | if !otp_locked_out? && send_otp_unlocked_email? 30 | send_otp_unlocked_email 31 | end 32 | end 33 | 34 | def after_otp_unlock_auth_failure 35 | super 36 | 37 | if send_otp_unlock_failed_email? 38 | send_otp_unlock_failed_email 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /doc/release_notes/1.6.0.txt: -------------------------------------------------------------------------------- 1 | = New Feature 2 | 3 | * An http_basic_auth feature has been added, allowing the use of 4 | HTTP Basic Auth to login. 5 | 6 | = New Configuration Options for jwt Feature 7 | 8 | * jwt_session_hash has been added, for modifying the hash given before 9 | creating the JWT. This can be used for setting JWT claims. 10 | Example: 11 | 12 | jwt_session_hash do 13 | super().merge(:exp=>Time.now.to_i + 120) 14 | end 15 | 16 | * jwt_decode_opts has been added for specifying additional options to 17 | JWT.decode. Among other things, this allows for JWT claim 18 | verification. Example: 19 | 20 | jwt_decode_opts(:verify_expiration=>true) 21 | 22 | * jwt_session_key has been added, specifying a key in the JWT that 23 | will be used to store session information, instead of storing 24 | session keys in the root of the JWT. Use of this option can avoid 25 | issues with reserved JWT claim names, and will probably be enabled 26 | by default starting in Rodauth 2. 27 | 28 | * jwt_symbolize_deeply? configuration method has been added, for 29 | whether to symbolize nested keys when decoding a JWT session hash. 30 | 31 | = Other Improvements 32 | 33 | * The reset_password feature no longer attempts to render a template 34 | in json-only mode. 35 | 36 | * The jwt_payload method is now memoized by default. 37 | 38 | -------------------------------------------------------------------------------- /www/pages/index.erb: -------------------------------------------------------------------------------- 1 |

Rodauth: Ruby's Most Advanced Authentication Framework

2 |
3 |
4 |
# cat config.ru
 5 | require "roda"
 6 | 
 7 | class RodauthApp < Roda
 8 |   secret = ENV['SESSION_SECRET']
 9 |   plugin :sessions, secret: secret
10 | 
11 |   # If using Rodauth in a non-Roda application
12 |   # plugin :middleware
13 | 
14 |   # JSON API
15 |   #plugin :json
16 |   #plugin :json_parser
17 | 
18 |   plugin :rodauth do
19 |     enable :login, :logout, :verify_account
20 |     enable :webauthn, :otp, :recovery_codes
21 |     hmac_secret secret
22 | 
23 |     # JSON API
24 |     #enable :jwt
25 |     #jwt_secret secret
26 |     #only_json? false
27 |   end
28 | 
29 |   route do |r|
30 |     r.rodauth
31 | 
32 |     rodauth.require_authentication
33 | 
34 |     # If using Rodauth in a Roda application
35 |     # Your app code here
36 |   end
37 | end
38 | 
39 | # If using Rodauth in a non-Roda application
40 | # use RodauthApp
41 | 
42 | # If using Rodauth in a Roda application
43 | run RodauthApp
44 | 
45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |

Rodauth is Ruby's most advanced authentication framework. Find out why you should use it.

53 | -------------------------------------------------------------------------------- /doc/guides/password_confirmation.rdoc: -------------------------------------------------------------------------------- 1 | = Require password confirmation for certain actions 2 | 3 | You might want to require the user to enter their password before accessing 4 | sensitive sections of the app. This functionality is provided by the confirm 5 | password feature, which accompanied with the password grace period feature will 6 | remember the entered password for a period of time: 7 | 8 | plugin :rodauth do 9 | enable :confirm_password, :password_grace_period 10 | 11 | # Remember the password for 1 hour 12 | password_grace_period 60*60 13 | end 14 | 15 | route do |r| 16 | r.rodauth 17 | 18 | r.is 'some-action' do 19 | # Require password authentication if the password has not been 20 | # input recently. 21 | rodauth.require_password_authentication 22 | 23 | # ... 24 | end 25 | end 26 | 27 | You can also do this for Rodauth actions that normally require a password. 28 | Which essentially moves the password confirmation into a separate step, as 29 | Rodauth's behavior with the password grace period feature is to ask for the 30 | password on the same form. 31 | 32 | plugin :rodauth do 33 | enable :confirm_password, :password_grace_period, :change_login, :change_password 34 | 35 | before_change_login_route { require_password_authentication } 36 | before_change_password_route { require_password_authentication } 37 | end 38 | -------------------------------------------------------------------------------- /lib/rodauth/features/password_pepper.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:password_pepper, :PasswordPepper) do 5 | depends :login_password_requirements_base 6 | 7 | auth_value_method :password_pepper, nil 8 | auth_value_method :previous_password_peppers, [""] 9 | auth_value_method :password_pepper_update?, true 10 | 11 | def password_match?(password) 12 | if (result = super) && @previous_pepper_matched && password_pepper_update? 13 | set_password(password) 14 | end 15 | 16 | result 17 | end 18 | 19 | def password_hash(password) 20 | super(password + password_pepper.to_s) 21 | end 22 | 23 | private 24 | 25 | def password_hash_match?(hash, password) 26 | return super if password_pepper.nil? 27 | 28 | return true if super(hash, password + password_pepper) 29 | 30 | @previous_pepper_matched = previous_password_peppers.any? do |pepper| 31 | super(hash, password + pepper) 32 | end 33 | end 34 | 35 | def database_function_password_match?(name, hash_id, password, salt) 36 | return super if password_pepper.nil? 37 | 38 | return true if super(name, hash_id, password + password_pepper, salt) 39 | 40 | @previous_pepper_matched = previous_password_peppers.any? do |pepper| 41 | super(name, hash_id, password + pepper, salt) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /doc/guides/login_return.rdoc: -------------------------------------------------------------------------------- 1 | = Redirect to original page after login 2 | 3 | When the user attempts to open a page that requires authentication, Rodauth 4 | redirects them to the login page. It can be useful to redirect them back to 5 | the page they originally requested after successful login. Similarly, you 6 | can do this for pages requiring multifactor authentication. 7 | 8 | plugin :rodauth do 9 | enable :login, :logout, :otp 10 | 11 | # Have successful login redirect back to originally requested page 12 | login_return_to_requested_location? true 13 | 14 | # Have successful multifactor authentication redirect back to 15 | # originally requested page 16 | two_factor_auth_return_to_requested_location? true 17 | end 18 | 19 | You can manually set which page to redirect after login or multifactor 20 | authentication, though it is questionable whether the user will desire 21 | this behavior compared to the default. 22 | 23 | route do |r| 24 | r.rodauth 25 | 26 | # Return the last visited path after login 27 | if rodauth.logged_in? 28 | # Return to the last visited page after multifactor authentication 29 | unless rodauth.two_factor_authenticated? 30 | session[rodauth.two_factor_auth_redirect_session_key] = request.fullpath 31 | end 32 | else 33 | session[rodauth.login_redirect_session_key] = request.fullpath 34 | end 35 | 36 | # rest of routes 37 | end 38 | -------------------------------------------------------------------------------- /doc/release_notes/2.21.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * When using the verify_account_grace_period feature, if the grace 4 | period has expired for currently logged in session, require_login 5 | will clear the session and redirect to the login page. This is 6 | implemented by having the unverified_account_session_key store the 7 | time of expiration, as an integer. 8 | 9 | * The previously private require_account method is now public. The 10 | method is used internally by Rodauth to check that not only is the 11 | current session logged in, but also that the account related to the 12 | currently logged in session still exists in the database. The only 13 | reason you would want to call require_account instead of 14 | require_authentication is if you want to handle cases where there 15 | can be logged in sessions for accounts that have been deleted. 16 | 17 | * Rodauth now avoids an unnecessary bcrypt hash calculation when 18 | updating accounts when using the account_password_hash_column 19 | configuration method. 20 | 21 | * When WebAuthn token last use times are displayed, Rodauth now uses a 22 | fixed format of YYYY-MM-DD HH:MM:SS, instead of relying on 23 | Time#to_s. If this presents an problem for your application, please 24 | open an issue and we can add a configuration method to control 25 | the behavior. 26 | 27 | * A typo in the default value of global_logout_label in the 28 | active_sessions feature has been fixed. 29 | -------------------------------------------------------------------------------- /lib/rodauth/features/webauthn_verify_account.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:webauthn_verify_account, :WebauthnVerifyAccount) do 5 | depends :verify_account, :webauthn 6 | 7 | def verify_account_view 8 | webauthn_setup_view 9 | end 10 | 11 | def create_account_set_password? 12 | false 13 | end 14 | 15 | def verify_account_set_password? 16 | false 17 | end 18 | 19 | def autologin_session(autologin_type) 20 | super 21 | if autologin_type == 'verify_account' 22 | set_session_value(authenticated_by_session_key, ['webauthn']) 23 | remove_session_value(autologin_type_session_key) 24 | webauthn_update_session(@webauthn_credential.id) 25 | end 26 | end 27 | 28 | private 29 | 30 | def before_verify_account 31 | super 32 | if features.include?(:json) && use_json? && !param_or_nil(webauthn_setup_param) 33 | cred = new_webauthn_credential 34 | json_response[webauthn_setup_param] = cred.as_json 35 | json_response[webauthn_setup_challenge_param] = cred.challenge 36 | json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge) 37 | end 38 | @webauthn_credential = webauthn_setup_credential_from_form_submission 39 | add_webauthn_credential(@webauthn_credential) 40 | end 41 | 42 | def webauthn_account_id 43 | super || account_id 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /doc/release_notes/2.1.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A check_csrf configuration method has been added for checking 4 | the CSRF token. This is useful in cases where the CSRF protection 5 | is provided by something other than the Roda route_csrf plugin. 6 | 7 | = Other Improvements 8 | 9 | * When using the http_basic_auth feature, logged_in? now checks for 10 | Basic authentication if the session is not already authenticated 11 | and Basic authentication has not yet been checked. This increases 12 | compatibility for applications that were using the http_basic_auth 13 | feature in Rodauth 1. 14 | 15 | * When creating accounts, the password field now correctly uses the 16 | new-password autocomplete attribute instead of the current-password 17 | autocomplete attribute. 18 | 19 | * When using the jwt feature, Rodauth no longer checks CSRF tokens 20 | in requests to Rodauth routes if the request submitted is a JSON 21 | request, includes a JWT, or Rodauth has been configured in JSON-only 22 | mode. 23 | 24 | * When using the verify_account_grace_period feature, if there is an 25 | unverified account without a password, do not consider the account 26 | open. Attempting to login into the account in such a case now 27 | shows a message letting the user know to verify the account. 28 | 29 | * The json_response_body configuration method is now used consistently 30 | in the jwt feature for all JSON responses. Previously, there were 31 | some cases that did not use it. 32 | -------------------------------------------------------------------------------- /doc/disallow_common_passwords.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Disallow Common Passwords Feature 2 | 3 | The disallow common passwords feature disallows setting of a password 4 | that matches one of the most common passwords. By default, a list of 5 | 10,000 of the most common passwords is used, but you can supply your 6 | own file. Using a larger list is recommended, but Rodauth doesn't 7 | ship with a larger list to avoid bloating the size of the gem. 8 | 9 | == Auth Value Methods 10 | 11 | most_common_passwords :: An object that responds to +include?+ which will return true if the password given is one of the most common passwords. Useful for custom password sets where they are not stored in files and kept in memory. 12 | most_common_passwords_file :: The path to the file containing the most common passwords, which are not allowed to be used for new passwords. Defaults to a list of 10,000 most common passwords that ships with Rodauth. Can be set to nil/false if you do not want to to load common passwords from a file. 13 | password_is_one_of_the_most_common_message :: The error message fragment to display if the given password matches one of the most common passwords. 14 | 15 | == Auth Methods 16 | 17 | password_one_of_most_common?(password) :: This can be used to override the default check for whether the given password is contained in the most_common_passwords_file. This method may be useful when using very large password databases where you don't want to keep the list of most common passwords in memory. 18 | -------------------------------------------------------------------------------- /doc/release_notes/2.11.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An :auth_class rodauth plugin option has been added, allowing a user 4 | to specify a specific Rodauth::Auth subclass to use, instead of 5 | always using a new subclass of Rodauth::Auth. This is designed for 6 | advanced configurations or other frameworks that build on top of 7 | Rodauth, which may want to customize the Rodauth::Auth subclasses to 8 | use. 9 | 10 | * Two additional configuration methods have been added for easier 11 | translatability, fixing issues where English text was hardcoded: 12 | 13 | * same_as_current_login_message (change_login feature) 14 | * contains_null_byte_message (login_password_requirements_base 15 | feature) 16 | 17 | = Other Improvements 18 | 19 | * Loading the rodauth plugin multiple times in the same application 20 | with different blocks now works better. The same context is now 21 | shared between the blocks, so you can load features in one block 22 | and call configuration methods added by the feature in the other 23 | block. Previously, you could only call configuration methods in 24 | the block that added the feature, and enabling a feature in a 25 | block that was already enabled in a previous block did not allow 26 | the use of configuration methods related to the feature. 27 | 28 | * Passing a block when loading the rodauth plugin is now optional. 29 | 30 | * The autocomplete attribute on the reset password form now uses 31 | new-password instead of current-password. 32 | -------------------------------------------------------------------------------- /doc/release_notes/1.11.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A rodauth.valid_jwt? method has been added, allowing for easy 4 | checking of whether a valid JWT has been submitted. If a valid 5 | JWT has been submitted, the contents of the JWT will be available 6 | in rodauth.session. 7 | 8 | * If using the jwt feature with json_response_custom_error_status? 9 | set to true, and going to a page that requires a login when not 10 | logged in, a 401 error status will now be used instead of a 400 11 | error status. You can customize this status using the new 12 | login_required_error_status configuration method. 13 | 14 | = Improvements 15 | 16 | * Time differences between the database server and the application 17 | server are now handled slightly better in the password_expiration 18 | feature. This mostly affects testing, where sometimes tests would 19 | previously fail if the database server time was ahead of the 20 | application server time when testing whether a password change was 21 | allowed. 22 | 23 | * Some methods that were private by default, but public if overridden, 24 | are now public by default. These include update_session and 25 | only_json? in the base feature, and json_request?, jwt_secret, and 26 | use_jwt? in the jwt feature. 27 | 28 | = Backwards Compatibility 29 | 30 | * The private jwt_payload method in the jwt feature now returns false 31 | instead of redirecting on error. This should not affect the 32 | application unless the method was being called explicitly. 33 | -------------------------------------------------------------------------------- /lib/rodauth/features/disallow_common_passwords.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:disallow_common_passwords, :DisallowCommonPasswords) do 5 | depends :login_password_requirements_base 6 | 7 | auth_value_method :most_common_passwords_file, File.expand_path('../../../../dict/top-10_000-passwords.txt', __FILE__) 8 | translatable_method :password_is_one_of_the_most_common_message, "is one of the most common passwords" 9 | auth_value_method :most_common_passwords, nil 10 | 11 | auth_methods :password_one_of_most_common? 12 | 13 | def password_meets_requirements?(password) 14 | super && password_not_one_of_the_most_common?(password) 15 | end 16 | 17 | def post_configure 18 | super 19 | 20 | return if most_common_passwords || !most_common_passwords_file 21 | 22 | require 'set' 23 | most_common = Set.new(File.read(most_common_passwords_file).split("\n").each(&:freeze)).freeze 24 | self.class.send(:define_method, :most_common_passwords){most_common} 25 | end 26 | 27 | def password_one_of_most_common?(password) 28 | most_common_passwords.include?(password) 29 | end 30 | 31 | private 32 | 33 | def password_not_one_of_the_most_common?(password) 34 | return true unless password_one_of_most_common?(password) 35 | set_password_requirement_error_message(:password_is_one_of_the_most_common, password_is_one_of_the_most_common_message) 36 | false 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /www/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rodauth: <%= title == 'index' ? "Ruby's Most Advanced Authentication Framework" : title.capitalize %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 27 |
28 | <%= content %> 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /doc/release_notes/2.7.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An auto_remove_recovery_codes? configuration method has been added 4 | to the recovery_codes feature. This will automatically remove 5 | recovery codes when the last multifactor authentication type other 6 | than the recovery codes has been removed. 7 | 8 | * The jwt_access_expired_status and expired_jwt_access_token_message 9 | configuration methods have been added to the jwt_refresh feature, 10 | for supporting custom statuses and messages for expired tokens. 11 | 12 | = Other Improvements 13 | 14 | * Rodauth will no longer attempt to require a feature that has 15 | already been required. Related to this is you can now use a 16 | a custom Rodauth feature without a rodauth/features/*.rb file 17 | in the Ruby library path, as long as you load the feature 18 | manually. 19 | 20 | * Rodauth now avoids method redefinition warnings in verbose 21 | warning mode. As Ruby 3 is dropping uninitialized instance 22 | variable warnings, Rodauth will be verbose warning free in 23 | Ruby 3. 24 | 25 | = Backwards Compatibility 26 | 27 | * The default remember cookie path is now set to '/'. This fixes 28 | usage in the case where rodauth is loaded under a subpath of the 29 | application (which is not the default behavior). Unfortunately, 30 | this change can negatively affect cases where multiple rodauth 31 | configurations are used in separate paths on the same domain. 32 | In these cases, you should now use remember_cookie_options and 33 | include a :path option. 34 | -------------------------------------------------------------------------------- /doc/release_notes/1.23.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * When the email_auth feature is used, the link to request email 4 | authentication is now displayed if the user inputs an incorrect 5 | password. Previously, it was only shown if the user had not 6 | yet entered a password. 7 | 8 | * A send_email configuration method has been added, which can be 9 | overridden to customize email delivery (such as logging such 10 | email). The configuration method block accepts a Mail::Message 11 | argument. 12 | 13 | * All rodauth.*_route methods that return the name of the route 14 | segment now have rodauth.*_path and rodauth.*_url equivalents, 15 | which return the path and URL for the related routes, respectively. 16 | The rodauth.*_path methods are useful when constructing links to 17 | the related Rodauth pages on the same site, and the rodauth.*_url 18 | methods are useful for constructing link to the Rodauth pages from 19 | other sites or in email. 20 | 21 | = Other Improvements 22 | 23 | * Specs have been removed from the gem file, reducing gem size by 24 | over 20%. 25 | 26 | * rodauth.authenticated? now returns true on the OTP setup page 27 | when using the otp feature. Previously, this method returned 28 | false on the OTP setup page. However, as the user has not yet 29 | setup OTP when viewing this page, they should be considered 30 | fully authenticated, as they would be if they viewed any other 31 | page before setting up OTP. This change probably only affects 32 | cases where the layout uses rodauth.authenticated?. 33 | -------------------------------------------------------------------------------- /www/pages/development.erb: -------------------------------------------------------------------------------- 1 |

Development

2 | 3 |

Rodauth, while a mature framework, is still under active development. You can join in on the discussions, ask questions, suggest features, and discuss Rodauth in general on GitHub Discussions or by joining the rodauth Google Group.

4 | 5 |

Reporting Bugs

6 | 7 |

To report a bug in Rodauth, use GitHub Issues. If you aren't sure if something is a bug, post a question on GitHub Discussions or the Google Group. Note that GitHub Issues should not be used to ask questions about how to use Rodauth; use GitHub Discussions or the Google Group for that.

8 | 9 |

Source Code

10 | 11 |

The master source code repository is jeremyevans/rodauth on GitHub.

12 | 13 |

Submitting Patches

14 | 15 |

The easiest way to contribute is to use Git, post the changes to a public repository, and send a pull request, either via GitHub, or the Google Group. Posting patches to the bug tracker or the Google Group works fine as well.

16 | 17 |

License

18 | 19 |

Rodauth is distributed under the MIT License. Patches are assumed to be submitted under the same license as Rodauth.

20 | 21 | -------------------------------------------------------------------------------- /doc/release_notes/2.36.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An otp_unlock feature has been added, allowing a user to unlock 4 | TOTP authentication with 3 consecutive successful TOTP 5 | authentications. Previously, once TOTP authentication was locked 6 | out, there was no way for the user to unlock it. 7 | 8 | Any unsuccessful TOTP authentication during the unlock process 9 | prevents unlocks attempts for a configurable amount of time (15 10 | minutes by default). By default, this limits brute force attempts 11 | to unlock TOTP authentication to less than 10^2 per day, with the 12 | odds of a successful unlock in each attempt being 1 in 10^18. 13 | 14 | * An otp_lockout_email feature has been added for emailing the user 15 | when their TOTP authentication has been locked out or unlocked, and 16 | when there has been a failed unlock attempt. 17 | 18 | * An otp_modify_email feature has been added for emailing the user 19 | when TOTP authentication has been setup or disabled for their 20 | account. 21 | 22 | * A webauthn_modify_email feature has been added for emailing the 23 | user when a WebAuthn authenticator has been added or removed from 24 | their account. 25 | 26 | * An account_from_id configuration method has been added for loading 27 | the account with the given account id. 28 | 29 | * A strftime_format configuration method has been added for 30 | configuring how Time values are formatted for display to the user. 31 | 32 | = Improvements 33 | 34 | * The internal_request feature now works with Roda's path_rewriter 35 | plugin. 36 | -------------------------------------------------------------------------------- /doc/webauthn_modify_email.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for WebAuthn Modify Email Feature 2 | 3 | The webauthn_modify_email feature emails users when a WebAuthn authenticator is added to or removed from their account. 4 | 5 | The webauthn_modify_email feature depends on the webauthn and email_base features. 6 | 7 | == Auth Value Methods 8 | 9 | webauthn_authenticator_added_email_body :: Body to use for the email notifying user that a WebAuthn authenticator has been added to their account. 10 | webauthn_authenticator_added_email_subject :: Subject to use for the email notifying user that a WebAuthn authenticator has been added to their account. 11 | webauthn_authenticator_removed_email_body :: Body to use for the email notifying user that a WebAuthn authenticator has been removed from their account. 12 | webauthn_authenticator_removed_email_subject :: Subject to use for the email notifying user that a WebAuthn authenticator has been removed from their account. 13 | 14 | == Auth Methods 15 | 16 | create_webauthn_authenticator_added_email :: A Mail::Message for the email notifying user that a WebAuthn authenticator has been added to their account. 17 | create_webauthn_authenticator_removed_email :: A Mail::Message for the email notifying user that a WebAuthn authenticator has been removed from their account. 18 | send_webauthn_authenticator_added_email :: Send the email notifying user that a WebAuthn authenticator has been added to their account. 19 | send_webauthn_authenticator_removed_email :: Send the email notifying user that a WebAuthn authenticator has been removed from their account. 20 | -------------------------------------------------------------------------------- /doc/release_notes/2.27.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * Token ids submitting in requests are now converted to integers if 4 | the configuration uses an integer primary key for the accounts 5 | table. If the configuration uses a non-integer primary key for 6 | the accounts table, the convert_token_id configuration method can 7 | be used, which should return the token id converted to the 8 | appropriate type, or nil if the token id is not valid for the type. 9 | 10 | This revised handling avoids raising a database error when an 11 | invalid token is submitted. 12 | 13 | * The button template can now be overridden in the same way that 14 | other Rodauth templates can be overridden. 15 | 16 | * When using the Bootstrap CSS framework, the text field in the 17 | Webauthn setup and auth forms is automatically hidden. The text 18 | field already had a rodauth-hidden class to make it easy to hide 19 | when using other CSS frameworks. 20 | 21 | * The email_from and email_to methods are now public instead of 22 | private. 23 | 24 | * A nicer error is raised if the Sequel Database object is missing. 25 | 26 | * A regression in the TOTP QR output that resulted in the QR codes 27 | being solid black squares has been fixed (this was fixed in 28 | Rodauth 2.26.1). 29 | 30 | = Backwards Compatibility 31 | 32 | * The webauth_credentials_for_get method in the webauthn feature has 33 | been renamed to webauthn_credentials_for_get for consistency with 34 | other methods. The webauth_credentials_for_get method will still 35 | work until Rodauth 3, but will issue deprecation warnings. 36 | -------------------------------------------------------------------------------- /doc/guides/paths.rdoc: -------------------------------------------------------------------------------- 1 | = Change route path 2 | 3 | You can change the URL path of any Rodauth route by overriding the 4 | corresponding *_route method: 5 | 6 | plugin :rodauth do 7 | enable :login, :logout, :create_account, :reset_password 8 | 9 | # Change login route to "/signin" 10 | login_route "signin" 11 | 12 | # Change redirect when login is required to "/signin" 13 | require_login_redirect { login_path } 14 | 15 | # Change create account route to "/register" 16 | create_account_route "register" 17 | 18 | # Change password reset request route to "/reset-password/request" 19 | reset_password_request_route "reset-password/request" 20 | end 21 | 22 | If you want to add a prefix to all Rodauth routes, you should use the +prefix+ 23 | setting: 24 | 25 | plugin :rodauth do 26 | enable :login, :logout 27 | 28 | # Use /auth prefix to each Rodauth route 29 | prefix "/auth" 30 | end 31 | 32 | route do |r| 33 | r.on "auth" do 34 | # Serve Rodauth routes under the /auth branch of the routing tree 35 | r.rodauth 36 | end 37 | 38 | # ... 39 | end 40 | 41 | There are cases where you may want to disable certain routes. For example, you 42 | may want to enable the create_account feature to allow creating admins, but 43 | only make it possible programmatically via internal requests. In this case, 44 | you should set the corresponding *_route method to +nil+: 45 | 46 | plugin :rodauth, name: :admin do 47 | enable :create_account 48 | 49 | # disable the /create-account route 50 | create_account_route nil 51 | end 52 | -------------------------------------------------------------------------------- /lib/rodauth/features/password_grace_period.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:password_grace_period, :PasswordGracePeriod) do 5 | auth_value_method :password_grace_period, 300 6 | session_key :last_password_entry_session_key, :last_password_entry 7 | 8 | auth_methods :password_recently_entered? 9 | 10 | def modifications_require_password? 11 | return false unless super 12 | !password_recently_entered? 13 | end 14 | 15 | def password_match?(_) 16 | if v = super 17 | @last_password_entry = set_last_password_entry 18 | end 19 | v 20 | end 21 | 22 | def password_recently_entered? 23 | return false unless last_password_entry = session[last_password_entry_session_key] 24 | last_password_entry + password_grace_period > Time.now.to_i 25 | end 26 | 27 | def update_session 28 | super 29 | set_session_value(last_password_entry_session_key, @last_password_entry) if defined?(@last_password_entry) 30 | end 31 | 32 | private 33 | 34 | def after_create_account 35 | super if defined?(super) 36 | @last_password_entry = Time.now.to_i 37 | end 38 | 39 | def after_reset_password 40 | super if defined?(super) 41 | @last_password_entry = Time.now.to_i 42 | end 43 | 44 | def set_last_password_entry 45 | set_session_value(last_password_entry_session_key, Time.now.to_i) 46 | end 47 | 48 | def require_password_authentication? 49 | return true if defined?(super) && super 50 | !password_recently_entered? 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /doc/change_login.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Change Login Feature 2 | 3 | The change login feature implements a form that a user can use to 4 | change their login. 5 | 6 | == Auth Value Methods 7 | 8 | change_login_additional_form_tags :: HTML fragment containing additional form tags to use on the change login form. 9 | change_login_button :: The text to use for the change login button. 10 | change_login_error_flash :: The flash error to show for an unsuccessful login change. 11 | change_login_notice_flash :: The flash notice to show after a successful login change. 12 | change_login_page_title :: The page title to use on the change login form. 13 | change_login_redirect :: Where to redirect after a successful login change. 14 | change_login_requires_password? :: Whether a password is required when changing logins. 15 | change_login_route :: The route to the change login action. Defaults to +change-login+. 16 | same_as_current_login_message :: The error message to display if using the same value as the current login when changing the login. 17 | 18 | == Auth Methods 19 | 20 | after_change_login :: Run arbitrary code after successful login change. 21 | before_change_login :: Run arbitrary code before changing a login. 22 | before_change_login_route :: Run arbitrary code before handling a change login route. 23 | change_login(login) :: Change the users login to the given login, or return nil/false if the login cannot be changed to the given login. 24 | change_login_response :: Return a response after a successful login change. By default, redirects to +change_login_redirect+. 25 | change_login_view :: The HTML to use for the change login form. 26 | -------------------------------------------------------------------------------- /doc/release_notes/1.13.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A cache_templates configuration method has been added, which can be 4 | set to false to disable the default caching of templates. The main 5 | time you would want to use this is if you were overriding Rodauth's 6 | templates with your own templates and modifying such templates in 7 | development mode. If that is the case, you may want to use 8 | something like: 9 | 10 | cache_templates(ENV['RACK_ENV'] != 'development') 11 | 12 | * An invalid_previous_password_message configuration method has been 13 | added to the change_password feature, which overrides the default 14 | invalid_password_message configuration method if the incorrect 15 | previous password is used when changing the password. This is 16 | designed for use when invalid_password_message has been overridden 17 | and the message doesn't make sense in the change password case. 18 | 19 | * A json_response_body(hash) configuration method has been added to 20 | the jwt feature, allowing for custom formatting of the JSON 21 | response body. This is called with the hash to use in the 22 | response, and should return a JSON-formatted string. Example: 23 | 24 | json_response_body do |hash| 25 | super('status'=>response.status, 'detail'=>hash) 26 | end 27 | 28 | = Other Improvements 29 | 30 | * In the jwt feature, if json_response_custom_error_status? is set to 31 | true, custom error statuses will be used if only_json? is set to 32 | true, even if the request is not in JSON format. Previously, 33 | custom error statuses were only used if the request was in JSON 34 | format. 35 | -------------------------------------------------------------------------------- /javascript/webauthn_autofill.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; 3 | var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); }; 4 | var element = document.getElementById('webauthn-login-form'); 5 | 6 | if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) return; 7 | 8 | PublicKeyCredential.isConditionalMediationAvailable().then(function(available) { 9 | if (!available) return; 10 | 11 | var opts = JSON.parse(element.getAttribute("data-credential-options")); 12 | opts.challenge = unpack(opts.challenge); 13 | opts.allowCredentials.forEach(function(cred) { cred.id = unpack(cred.id); }); 14 | 15 | navigator.credentials.get({mediation: "conditional", publicKey: opts}).then(function(cred) { 16 | var rawId = pack(cred.rawId); 17 | var authValue = { 18 | type: cred.type, 19 | id: rawId, 20 | rawId: rawId, 21 | response: { 22 | authenticatorData: pack(cred.response.authenticatorData), 23 | clientDataJSON: pack(cred.response.clientDataJSON), 24 | signature: pack(cred.response.signature) 25 | } 26 | }; 27 | 28 | if (cred.response.userHandle) { 29 | authValue.response.userHandle = pack(cred.response.userHandle); 30 | } 31 | 32 | document.getElementById('webauthn-auth').value = JSON.stringify(authValue); 33 | 34 | element.submit(); 35 | }); 36 | }); 37 | })(); 38 | 39 | -------------------------------------------------------------------------------- /doc/release_notes/2.6.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An around_rodauth configuration method has been added, which is 4 | called around all Rodauth actions. This configuration method 5 | is passed a block, and is useful for cases where you want to wrap 6 | Rodauth's handling of the request. 7 | 8 | For example, if you had a method named time_block in your Roda scope 9 | that timed block execution and added a response header, you could 10 | time Rodauth actions using something like: 11 | 12 | around_rodauth do |&block| 13 | scope.time_block('Rodauth') do 14 | super(&block) 15 | end 16 | end 17 | 18 | * The allow_refresh_with_expired_jwt_access_token? configuration has 19 | been added to the jwt_refresh feature, allowing refreshing with an 20 | expired but otherwise valid access token. When using this method, 21 | it is required to have an hmac_secret specified, so that Rodauth 22 | can make sure the access token matches the refresh token. 23 | 24 | = Other Improvements 25 | 26 | * The javascript for setting up a WebAuthn token has been fixed to 27 | allow it to work correctly if there is already an existing 28 | WebAuthn token for the account. 29 | 30 | * The rodauth.setup_account_verification method has been promoted to 31 | public API. You can use this method for automatically sending 32 | account verification emails when automatically creating accounts. 33 | 34 | * Rodauth no longer loads the same feature multiple times into a 35 | single configuration. This didn't cause any problems before, but 36 | could result in duplicate entries when looking at the loaded 37 | features. 38 | -------------------------------------------------------------------------------- /doc/release_notes/2.22.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * Rodauth now ignores parameters containing ASCII NUL bytes ("\0") by 4 | default. You can customize this behavior using the 5 | null_byte_parameter_value configuration method. 6 | 7 | * A reset_password_notify feature has been added for emailing users 8 | after successful password resets. 9 | 10 | * External features can now use the email method inside their 11 | feature definitions to DRY up the creation of email configuration 12 | methods. The email method will setup the following configuration 13 | methods for the feature: 14 | 15 | * ${name}_email_subject 16 | * ${name}_email_body 17 | * create_${name}_email 18 | * send_${name}_email 19 | 20 | = Other Improvements 21 | 22 | * The active_sessions feature now correctly handles logouts for 23 | sessions that were created before the active_sessions feature was 24 | added to the Rodauth configuration. 25 | 26 | * The change_password_notify feature now works correctly when using 27 | template precompilation. 28 | 29 | * The update_sms method now updates the in-memory sms hash instead of 30 | the in-memory account hash. This only has an effect if you are 31 | using the sms_codes feature and customizing Rodauth to access one 32 | of these hashes after a call to update_sms. 33 | 34 | = Backwards Compatibility 35 | 36 | * If your application requires the ability to submit values containing 37 | ASCII NUL bytes ("\0") as Rodauth parameters, you should use the 38 | new null_byte_parameter_value configuration method to pass the 39 | value through unchanged: 40 | 41 | null_byte_parameter_value do |_, v| 42 | v 43 | end 44 | -------------------------------------------------------------------------------- /doc/release_notes/2.3.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * Configuration methods have been added for easier validation of 4 | logins when logins must be valid email addresses (the default): 5 | 6 | * login_valid_email?(login) can be used for full control of 7 | determining whether the login is valid. 8 | 9 | * login_email_regexp can be used to set the regexp used in the 10 | default login_valid_email? check. 11 | 12 | * login_not_valid_email_message can be used to set the field 13 | error message if the login is not a valid email. Previously, this 14 | value was hardcoded and not translatable. 15 | 16 | * The {create,drop}_database_authentication_functions now work 17 | correctly with uuid keys on PostgreSQL. All other parts of 18 | Rodauth already worked correctly with uuid keys. 19 | 20 | = Other Improvements 21 | 22 | * The before_jwt_refresh_route hook is now called before the route 23 | is taken. Previously, the configuration method had no effect. 24 | 25 | * rodauth.login can now be used by external code to login the current 26 | account (the account that rodauth.account returns). This should be 27 | passed the authentication type string used to login, such as 28 | password. 29 | 30 | * The jwt_refresh route now returns an error for requests where a 31 | valid access token for a logged in session is not provided. You 32 | can use the jwt_refresh_without_access_token_message and 33 | jwt_refresh_without_access_token_status configuration methods 34 | to configure the error response. 35 | 36 | * The new refresh token is now available to the after_refresh_token 37 | hook by looking in json_response[jwt_refresh_token_key]. 38 | -------------------------------------------------------------------------------- /doc/session_expiration.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Session Expiration Feature 2 | 3 | The session expiration feature allows setting an inactivity timeout and a max 4 | lifetime for sessions. When this feature is used, you should use 5 | +rodauth.check_session_expiration+ at the top (or other appropriate place) 6 | in your routing tree. 7 | 8 | route do |r| 9 | rodauth.check_session_expiration 10 | r.rodauth 11 | 12 | # ... 13 | end 14 | 15 | When checking session expiration, if the last activity was more than the 16 | inactivity timeout, or the session was created more the maximum lifetime 17 | ago, the session is cleared, and the user is redirected to the login page. 18 | 19 | == Auth Value Methods 20 | 21 | max_session_lifetime :: The maximum number of seconds since session creation that sessions will be valid for, regardless of session activity. 86400 by default (1 day). 22 | session_created_session_key :: The session key storing the session creation timestamp. 23 | session_expiration_default :: Whether to expire sessions that don't have the created at or last activity at timestamps set, true by default. 24 | session_expiration_error_flash :: The flash error to show if a session expires. 25 | session_expiration_error_status :: The error status to use when a JSON request is made and the session has expired, 401 by default. 26 | session_expiration_redirect :: Where to redirect if a session expires. 27 | session_inactivity_timeout :: The maximum number of seconds allowed since the last activity before the session will be considered invalid. 1800 by default (30 minutes). 28 | session_last_activity_session_key :: The session key storing the last session activity timestamp. 29 | -------------------------------------------------------------------------------- /doc/release_notes/2.34.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * A rodauth.current_route method has been added for returning the route 4 | name symbol (if rodauth is currently handling the route). This makes it 5 | simpler to write code that extends Rodauth and works with 6 | applications that use override the default route names. 7 | 8 | * A remove_all_active_sessions_except_for method has been added to the 9 | active_sessions feature, which removes all active sessions for the 10 | current account, except for the session id given. 11 | 12 | * A remove_all_active_sessions_except_current method has been added to 13 | the active_sessions feature, which removes all active sessions for 14 | the current account, except for the current session. 15 | 16 | = Improvements 17 | 18 | * Rodauth now supports overriding webauthn_rp_id in the webauthn 19 | feature. 20 | 21 | * When using the login feature, Rodauth now defaults 22 | require_login_redirect to use the path to the login route, instead 23 | of /login. 24 | 25 | * When setting up multifactor authentication, Rodauth now handles the 26 | case where account has been deleted, instead of raising an exception. 27 | 28 | * When a database connection is not available during startup, Rodauth 29 | now handles that case instead of raising an exception. Note that in 30 | this case, Rodauth cannot automatically setup a conversion of token 31 | ids to integer, since it cannot determine whether the underlying 32 | database column uses an integer type. 33 | 34 | * When using WebAuthn 3+, Rodauth no longer defines singleton methods 35 | to work around limitations in WebAuthn. Instead, it uses public 36 | APIs that were added in WebAuthn 3. 37 | -------------------------------------------------------------------------------- /doc/close_account.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Close Account Feature 2 | 3 | The close account feature allows users to close their accounts. 4 | 5 | == Auth Value Methods 6 | 7 | account_closed_status_value :: The integer representing closed accounts. 8 | close_account_additional_form_tags :: HTML fragment containing additional form tags to use on the close account form. 9 | close_account_button :: The text to use for the close account button. 10 | close_account_error_flash :: The flash error to show if there is an error closing the account. 11 | close_account_notice_flash :: The flash notice to show after closing the account. 12 | close_account_page_title :: The page title to use on the close account form. 13 | close_account_redirect :: Where to redirect after closing the account. 14 | close_account_requires_password? :: Whether a password is required when closing accounts. 15 | close_account_route :: The route to the close account action. Defaults to +close-account+. 16 | delete_account_on_close? :: Whether to delete the account when closing it, default value is to use +skip_status_checks?+. 17 | 18 | == Auth Methods 19 | 20 | after_close_account :: Run arbitrary code after closing the account. 21 | before_close_account :: Run arbitrary code before closing an account. 22 | before_close_account_route :: Run arbitrary code before handling a close account route. 23 | close_account :: Close the account, by default setting the account status to closed. 24 | close_account_response :: Return a response after successfully closing the account . By default, redirects to +close_account_redirect+. 25 | close_account_view :: The HTML to use for the close account form. 26 | delete_account :: If +delete_account_on_close?+ is true, delete the account when closing it. 27 | -------------------------------------------------------------------------------- /doc/change_password.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Change Password Feature 2 | 3 | The change password feature implements a form that a user can use to 4 | change their password. 5 | 6 | == Auth Value Methods 7 | 8 | change_password_additional_form_tags :: HTML fragment containing additional form tags to use on the change password form. 9 | change_password_button :: The text to use for the change password button. 10 | change_password_error_flash :: The flash error to show for an unsuccessful password change. 11 | change_password_notice_flash :: The flash notice to show after a successful password change. 12 | change_password_page_title :: The page title to use on the change password form. 13 | change_password_redirect :: Where to redirect after a successful password change. 14 | change_password_requires_password? :: Whether a password is required when changing passwords. 15 | change_password_route :: The route to the change password action. Defaults to +change-password+. 16 | invalid_previous_password_message :: The message to use when the previous password was incorrect. Defaults to +invalid_password_message+. 17 | new_password_label :: The label to use for the new password. 18 | new_password_param :: The parameter name to use for new passwords. 19 | 20 | == Auth Methods 21 | 22 | after_change_password :: Run arbitrary code after successful password change. 23 | before_change_password :: Run arbitrary code before changing the password for an account. 24 | before_change_password_route :: Run arbitrary code before handling a change password route. 25 | change_password_response :: Return a response after a successful password change. By default, redirects to +change_password_redirect+. 26 | change_password_view :: The HTML to use for the change password form. 27 | -------------------------------------------------------------------------------- /lib/rodauth/features/jwt_cors.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:jwt_cors, :JwtCors) do 5 | depends :jwt 6 | 7 | auth_value_method :jwt_cors_allow_origin, false 8 | auth_value_method :jwt_cors_allow_methods, 'POST' 9 | auth_value_method :jwt_cors_allow_headers, 'Content-Type, Authorization, Accept' 10 | auth_value_method :jwt_cors_expose_headers, 'Authorization' 11 | auth_value_method :jwt_cors_max_age, 86400 12 | 13 | auth_methods(:jwt_cors_allow?) 14 | 15 | def jwt_cors_allow? 16 | return false unless origin = request.env['HTTP_ORIGIN'] 17 | 18 | case allowed = jwt_cors_allow_origin 19 | when String 20 | timing_safe_eql?(origin, allowed) 21 | when Array 22 | allowed.any?{|s| timing_safe_eql?(origin, s)} 23 | when Regexp 24 | allowed =~ origin 25 | when true 26 | true 27 | else 28 | false 29 | end 30 | end 31 | 32 | private 33 | 34 | def before_rodauth 35 | if jwt_cors_allow? 36 | set_response_header('access-control-allow-origin', request.env['HTTP_ORIGIN']) 37 | 38 | # Handle CORS preflight request 39 | if request.request_method == 'OPTIONS' 40 | set_response_header('access-control-allow-methods', jwt_cors_allow_methods) 41 | set_response_header('access-control-allow-headers', jwt_cors_allow_headers) 42 | set_response_header('access-control-max-age', jwt_cors_max_age.to_s) 43 | response.status = 204 44 | return_response 45 | end 46 | 47 | set_response_header('access-control-expose-headers', jwt_cors_expose_headers) 48 | end 49 | 50 | super 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /doc/release_notes/2.2.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * When using the jwt_refresh feature, you can remove the current 4 | refresh token when logging out by submitting the refresh token 5 | in the logout request, the same as when submitting the refresh 6 | token to obtain a new refresh token. You can also use a value 7 | of "all" instead of the refresh token to remove all refresh 8 | tokens when logging out. 9 | 10 | * A rodauth.otp_last_use method has been added to the otp feature, 11 | allowing you to determine when the otp was last used. 12 | 13 | = Other Improvements 14 | 15 | * When using multifactor authentication, rodauth.authenticated? and 16 | rodauth.require_authentication now cache values in the session and 17 | do not perform queries every time they are called. 18 | 19 | * Many guides for common scenarios have been added to the 20 | documentation. These augment Rodauth's existing comprehensive 21 | feature documentation, which is aimed to be more of a reference 22 | and less of a guide. 23 | 24 | * When the verify_account_grace_period and email_auth features are 25 | used with a multifactor authentication feature, and the 26 | verify_account_set_password? configuration method is set to true, 27 | Rodauth no longer raises a NoMethodError when checking if the 28 | session was authenticated. 29 | 30 | * In the verify_account feature, if verify_account_email_resend 31 | returns false indicating no email was sent, an error message 32 | is now used, instead of a success message. 33 | 34 | * In the password_complexity feature, the password_dictionary 35 | configuration method was previously ignored if the default 36 | password dictionary file existed. 37 | 38 | * Rodauth and all features that ship with it now have 100% branch 39 | coverage. 40 | -------------------------------------------------------------------------------- /doc/guides/password_requirements.rdoc: -------------------------------------------------------------------------------- 1 | = Customize password requirements 2 | 3 | By default, Rodauth requires passwords to have at least 6 characters. You can 4 | modify the minimum and maximum length: 5 | 6 | plugin :rodauth do 7 | enable :login, :logout, :create_account 8 | 9 | # Require passwords to have at least 8 characters 10 | password_minimum_length 8 11 | 12 | # Don't allow passwords to be too long, to prevent long password DoS attacks 13 | password_maximum_length 64 14 | end 15 | 16 | You can use the {disallow common passwords feature}[rdoc-ref:doc/disallow_common_passwords.rdoc] 17 | to prevent the usage of common passwords (the most common 10,000 by default). 18 | 19 | You can use additional complexity checks on passwords via the {password 20 | complexity feature}[rdoc-ref:doc/password_complexity.rdoc], though most of 21 | those complexity checks are no longer considered modern security best 22 | practices and are likely to decrease overall security. 23 | 24 | If you want complete control over whether passwords meet requirements, you 25 | can use the password_meets_requirements? configuration method. 26 | 27 | plugin :rodauth do 28 | enable :login, :logout, :create_account 29 | 30 | password_meets_requirements? do |password| 31 | super(password) && password_complex_enough?(password) 32 | end 33 | 34 | auth_class_eval do 35 | # If password doesn't pass custom validation, add field error with error 36 | # reason, and return false. 37 | def password_complex_enough?(password) 38 | return true if password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/) 39 | set_password_requirement_error_message(:password_simple, "requires one number and one special character") 40 | false 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/otp_modify_email_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | require 'rotp' 4 | 5 | describe 'Rodauth otp_lockout_email feature' do 6 | secret_length = (ROTP::Base32.respond_to?(:random_base32) ? ROTP::Base32.random_base32 : ROTP::Base32.random).length 7 | 8 | it "should email when otp authentication is locked out, unlocked, or has a failed unlock attempt" do 9 | rodauth do 10 | enable :login, :logout, :otp_modify_email 11 | hmac_secret '123' 12 | end 13 | roda do |r| 14 | r.rodauth 15 | 16 | r.redirect '/login' unless rodauth.logged_in? 17 | 18 | if rodauth.two_factor_authentication_setup? 19 | r.redirect '/otp-auth' unless rodauth.authenticated? 20 | view :content=>"With 2FA" 21 | else 22 | view :content=>"Without 2FA" 23 | end 24 | end 25 | 26 | login 27 | visit '/otp-setup' 28 | secret = page.html.match(/Secret: ([a-z2-7]{#{secret_length}})/)[1] 29 | totp = ROTP::TOTP.new(secret) 30 | fill_in 'Password', :with=>'0123456789' 31 | fill_in 'Authentication Code', :with=>totp.now 32 | click_button 'Setup TOTP Authentication' 33 | email = email_sent 34 | email.subject.must_equal "TOTP Authentication Setup" 35 | email.body.to_s.must_equal <'0123456789' 42 | click_button 'Disable TOTP Authentication' 43 | email = email_sent 44 | email.subject.must_equal "TOTP Authentication Disabled" 45 | email.body.to_s.must_equal <'foo@example.com', :password=>'0123456789').must_equal true 21 | rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'01234567').must_equal false 22 | rodauth.valid_login_and_password?(:login=>'foo3@example.com', :password=>'0123456789').must_equal false 23 | 24 | rodauth.create_account(:login=>'foo3@example.com', :password=>'sdkjnlsalkklsda').must_be_nil 25 | 26 | rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'0123456789').must_equal true 27 | rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'01234567').must_equal false 28 | rodauth.valid_login_and_password?(:login=>'foo3@example.com', :password=>'sdkjnlsalkklsda').must_equal true 29 | 30 | rodauth.change_password(:account_login=>'foo@example.com', :password=>'01234567').must_be_nil 31 | 32 | rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'0123456789').must_equal false 33 | rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'01234567').must_equal true 34 | rodauth.valid_login_and_password?(:login=>'foo3@example.com', :password=>'sdkjnlsalkklsda').must_equal true 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /doc/guides/alternative_login.rdoc: -------------------------------------------------------------------------------- 1 | = Use a non-email login 2 | 3 | Rodauth's by default uses email addresses for identifying users, since that is 4 | the most common form of identifier currently. In some cases, you might want 5 | to allow logging in via alternative identifiers, such as a username. In this 6 | case, it is best to choose a different column name for the login, such as 7 | +:username+. Among other things, this also makes it so that the login field 8 | does not expect an email address to be provided. 9 | 10 | plugin :rodauth do 11 | enable :login, :logout 12 | login_column :username 13 | end 14 | 15 | Note that Rodauth features that require sending email need an email address, and 16 | that defaults to the value of the login column. If you have both a username and 17 | an email for an account, you can have the login column be the user, and use the 18 | value of the email column for the email address. 19 | 20 | plugin :rodauth do 21 | enable :login, :logout, :reset_password 22 | 23 | login_column :username 24 | email_to do 25 | account[:email] 26 | end 27 | end 28 | 29 | An alternative approach would be to accept a login and automatically change it 30 | to an email address. If you have a +username+ field on the +accounts+ table, 31 | then you can configure Rodauth to allow entering a username instead of email 32 | during login. See the {Adding new registration field}[rdoc-ref:doc/guides/registration_field.rdoc] 33 | guide for instructions on requiring add an additional field during registration. 34 | 35 | plugin :rodauth do 36 | enable :login, :logout 37 | 38 | account_from_login do |login| 39 | # handle the case when login parameter is a username 40 | unless login.include?("@") 41 | login = db[:accounts].where(username: login).get(:email) 42 | end 43 | 44 | super(login) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /javascript/webauthn_setup.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; 3 | var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); }; 4 | var element = document.getElementById('webauthn-setup-form'); 5 | var f = function(e) { 6 | //console.log(e); 7 | e.preventDefault(); 8 | if (navigator.credentials) { 9 | var opts = JSON.parse(element.getAttribute("data-credential-options")); 10 | opts.challenge = unpack(opts.challenge); 11 | opts.user.id = unpack(opts.user.id); 12 | opts.excludeCredentials.forEach(function(cred) { cred.id = unpack(cred.id); }); 13 | //console.log(opts); 14 | navigator.credentials.create({publicKey: opts}). 15 | then(function(cred){ 16 | //console.log(cred); 17 | //window.cred = cred 18 | 19 | var rawId = pack(cred.rawId); 20 | document.getElementById('webauthn-setup').value = JSON.stringify({ 21 | type: cred.type, 22 | id: rawId, 23 | rawId: rawId, 24 | response: { 25 | attestationObject: pack(cred.response.attestationObject), 26 | clientDataJSON: pack(cred.response.clientDataJSON) 27 | } 28 | }); 29 | element.removeEventListener("submit", f); 30 | element.submit(); 31 | }). 32 | catch(function(e){document.getElementById('webauthn-setup-button').innerHTML = "Error creating public key in authenticator: " + e}); 33 | } else { 34 | document.getElementById('webauthn-setup-button').innerHTML = "WebAuthn not supported by browser, or browser has disabled it on this page"; 35 | } 36 | }; 37 | element.addEventListener("submit", f); 38 | })(); 39 | -------------------------------------------------------------------------------- /doc/disallow_password_reuse.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Disallow Password Reuse Feature 2 | 3 | The disallow password reuse feature disallows setting of a password 4 | that matches a number of previous passwords (6 by default). 5 | 6 | On databases where Rodauth supports the use of database authentication 7 | functions, Rodauth also supports the use of database functions for checking 8 | previous passwords, so previous password hashes enjoy the same database 9 | security as current password hashes. 10 | 11 | It is not recommended to use this feature unless you have a policy that 12 | requires it. This will significantly slow down setting a new password 13 | due to the need to check all of the previous stored passwords. Additionally, 14 | storing previous passwords means that if attackers can get access to the 15 | the database, they can get the previous stored passwords in addition to the 16 | current password. 17 | 18 | == Auth Value Methods 19 | 20 | password_same_as_previous_password_message :: The error message fragment to display if the given password is the same as a previous password. 21 | previous_password_account_id_column :: The column in the +previous_password_hash_table+ that stores the account id. 22 | previous_password_hash_column :: The column in the +previous_password_hash_table+ that stores the password hash. 23 | previous_password_hash_table :: The table storing previous password hashes. 24 | previous_password_id_column :: The column in the +previous_password_hash_table+ that stores the autoincrementing primary key. 25 | previous_passwords_to_check :: The number of previous password hashes to store and check. 26 | 27 | == Auth Methods 28 | 29 | add_previous_password_hash(hash) :: Add the given hash to the list of previous hashes for the current account. 30 | password_doesnt_match_previous_password?(password) :: Whether the password given matches any of the previous passwords. 31 | -------------------------------------------------------------------------------- /doc/release_notes/2.31.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * The internal_request feature now supports WebAuthn, using 4 | the following methods: 5 | 6 | * With the webauthn feature: 7 | * webauthn_setup_params 8 | * webauthn_setup 9 | * webauthn_auth_params 10 | * webauthn_auth 11 | * webauthn_remove 12 | 13 | * With the webauthn_login feature: 14 | * webauthn_login_params 15 | * webauthn_login 16 | 17 | * A webauthn_login_user_verification_additional_factor? configuration 18 | method has been added to the webauthn_login feature. By default, 19 | this method returns false. If you configure the method to return 20 | true, and the WebAuthn credential provided specifies that it 21 | verified the user, then this will treat the user verification as 22 | a second factor, so the user will be considered multifactor 23 | authenticated after successful login. You should only set this 24 | method to true if you consider the WebAuthn user verification 25 | strong enough to be a independent factor. 26 | 27 | * A json_response_error? configuration method has been added to the 28 | json feature. This should return whether the current response 29 | should be treated as an error by the json feature. By default, 30 | it is true if json_response_error_key is set in the response, 31 | since that is the default place that Rodauth stores errors when 32 | using the json feature. 33 | 34 | * A webauthn_invalid_webauthn_id_message configuration method has 35 | been added for customizing the error message used for invalid 36 | WebAuthn IDs. 37 | 38 | = Other Improvements 39 | 40 | * The argon2 feature now supports setting the Argon2 p_cost if 41 | argon2 2.1+ is installed. 42 | 43 | * An :invalid_webauthn_id error reason is now used for invalid 44 | WebAuthn IDs. 45 | 46 | * The clear_session method now works as expected for internal 47 | requests. 48 | -------------------------------------------------------------------------------- /lib/rodauth/features/session_expiration.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:session_expiration, :SessionExpiration) do 5 | error_flash "This session has expired, please login again" 6 | redirect{require_login_redirect} 7 | 8 | auth_value_method :max_session_lifetime, 86400 9 | session_key :session_created_session_key, :session_created_at 10 | auth_value_method :session_expiration_error_status, 401 11 | auth_value_method :session_expiration_default, true 12 | auth_value_method :session_inactivity_timeout, 1800 13 | session_key :session_last_activity_session_key, :last_session_activity_at 14 | 15 | def check_session_expiration 16 | return unless logged_in? 17 | 18 | unless session.has_key?(session_last_activity_session_key) && session.has_key?(session_created_session_key) 19 | if session_expiration_default 20 | expire_session 21 | end 22 | 23 | return 24 | end 25 | 26 | time = Time.now.to_i 27 | 28 | if session[session_last_activity_session_key] + session_inactivity_timeout < time 29 | expire_session 30 | end 31 | set_session_value(session_last_activity_session_key, time) 32 | 33 | if session[session_created_session_key] + max_session_lifetime < time 34 | expire_session 35 | end 36 | end 37 | 38 | def expire_session 39 | clear_session 40 | set_redirect_error_status session_expiration_error_status 41 | set_error_reason :session_expired 42 | set_redirect_error_flash session_expiration_error_flash 43 | redirect session_expiration_redirect 44 | end 45 | 46 | def update_session 47 | super 48 | t = Time.now.to_i 49 | set_session_value(session_last_activity_session_key, t) 50 | set_session_value(session_created_session_key, t) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /doc/jwt_cors.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for JWT CORS Feature 2 | 3 | The jwt_cors feature adds support for Cross-Origin Resource Sharing 4 | to Rodauth's JSON API. 5 | 6 | When this feature is used, CORS requests are handled. This includes 7 | CORS preflight requests, which are required since Rodauth's JSON API 8 | uses the application/json request content type. 9 | 10 | This feature depends on the jwt feature. 11 | 12 | == Auth Value Methods 13 | 14 | jwt_cors_allow_headers :: For allowed CORS-preflight requests, the value returned in the Access-Control-Allow-Headers header (default: 'Content-Type, Authorization, Accept'). This specifies which headers can be included in CORS requests. 15 | jwt_cors_allow_methods :: For allowed CORS-preflight requests, the value returned in the Access-Control-Allow-Methods header (default: 'POST'). This specifies which methods are allowed in CORS requests. 16 | jwt_cors_allow_origin :: Which origins are allowed to perform CORS requests. The default is +false+. This can be a String, Array of Strings, Regexp, or +true+ to allow CORS requests from any domain. 17 | jwt_cors_expose_headers :: For allowed CORS requests, the value returned in the Access-Control-Expose-Headers header (default: 'Authorization'). This specifies which headers the browser is allowed to access from a response to a CORS request. 18 | jwt_cors_max_age :: For allowed CORS-preflight requests, the value returned in the Access-Control-Max-Age header (default: 86400). This specifies how long before the information returned should be considered stale and another CORS preflight request made. 19 | 20 | == Auth Methods 21 | 22 | jwt_cors_allow? :: Whether the request should be allowed. This is called for all requests for a Rodauth route that include an Origin header. It should return true or false for whether to specially handle the cross-origin request. By default, uses the +jwt_cors_allow_origin+ setting to check the origin. 23 | -------------------------------------------------------------------------------- /doc/guides/admin_activation.rdoc: -------------------------------------------------------------------------------- 1 | = Require account verification by admin 2 | 3 | There are scenarios in which, instead of allowing the user to verify they have 4 | access to the email for the account, you may want to have an admin or moderator 5 | approve new accounts manually. One way this can be achieved by sending the 6 | account verification email to the admin: 7 | 8 | plugin :rodauth do 9 | enable :login, :logout, :verify_account, :reset_password 10 | 11 | # Send account verification email to the admin 12 | email_to do 13 | if account[account_status_column] == account_unverified_status_value 14 | "admin@myapp.com" 15 | else 16 | super() 17 | end 18 | end 19 | 20 | # Do not ask for password when creating or verifying account 21 | verify_account_set_password? false 22 | create_account_set_password? false 23 | 24 | # Adjust the account verification email subject and body 25 | verify_account_email_subject "New User Awaiting Admin Approval" 26 | verify_account_email_body do 27 | "The user #{account[login_column]} has created an account. Click here to approve it: #{verify_account_email_link}." 28 | end 29 | 30 | # Display this message to the user after they've created their account 31 | verify_account_email_sent_notice_flash "Your account has been created and is awaiting approval" 32 | 33 | # Prevent the admin from being logged in after confirming the account 34 | verify_account_autologin? false 35 | verify_account_notice_flash "The account has been approved" 36 | 37 | # Send a reset password email after verifying the account. 38 | # This allows the user to choose the password for the account, 39 | # and also makes sure the user can only log in if they have 40 | # access to the email address for the account. 41 | after_verify_account do 42 | generate_reset_password_key_value 43 | create_reset_password_key 44 | send_reset_password_email 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /doc/release_notes/2.38.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * Rodauth now automatically supports fixed locals in templates if 4 | using Roda 3.88+ and Tilt 2.6+. This allows you to use the 5 | Roda default_fixed_locals: '()' template option without breaking 6 | Rodauth. If the default fixed locals support breaks your Rodauth 7 | configuration, such as if you are overriding Rodauth templates 8 | and modifying the local variables they accept, you can disable 9 | the use of fixed locals in your Rodauth configuration: 10 | 11 | use_template_fixed_locals? false 12 | 13 | * Rodauth::ConfigurationError has been added, and issues that 14 | Rodauth believes are configuration errors now use this 15 | exception class. 16 | 17 | = Other Improvements 18 | 19 | * The following methods are now public: 20 | 21 | * has_password? 22 | * email_auth_email_recently_sent? 23 | * unlock_account_email_recently_sent? 24 | * reset_password_email_recently_sent? 25 | * verify_account_email_recently_sent? 26 | 27 | This makes it supported to call these methods and use the result 28 | in your own code. 29 | 30 | * The verify-account-resend page now works if 31 | verify_account_resend_explanatory_text calls 32 | verify_account_email_recently_sent?. Rodauth does not do that 33 | by default, but if you override 34 | verify_account_resend_explanatory_text to use different text 35 | depending on whether the email was recently sent, direct 36 | navigations to the verify-account-resend page previously failed. 37 | 38 | * Rodauth now uses JWT.gem_version to check the JWT gem version, which 39 | works with JWT 2.10.0. JWT 2.10.1 restored the constants Rodauth 40 | used to check the version, but this allows the JWT to remove 41 | such constants again in the future without breaking Rodauth. 42 | 43 | = Backwards Compatibility 44 | 45 | * The change to use Rodauth::ConfigurationError can break code that 46 | rescued other exception classes, such as ArgumentError, 47 | RuntimeError, or NotImplementedError. 48 | -------------------------------------------------------------------------------- /demo-site/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Rodauth Demo - <%= @page_title %> 8 | 24 | 25 | 26 | 27 | 28 | 40 | 41 |
42 | <% if sms = last_sms_sent %> 43 |

<%= sms %>

44 | <% end %> 45 | <% if flash['notice'] %> 46 |

<%= flash['notice'] %>

47 | <% end %> 48 | <% if flash['error'] %> 49 |

<%= flash['error'] %>

50 | <% end %> 51 |

<%= @page_title %>

52 | 53 | <%== yield %> 54 | 55 | <% if mail = last_mail_sent %> 56 |

Last Email Sent

57 |
From: <%= mail.from.join %>
58 | To: <%= mail.to.join %>
59 | Subject: <%= mail.subject %>
60 | 
61 | <%= mail.body %>
62 | <% end %> 63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /javascript/webauthn_auth.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; 3 | var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); }; 4 | var element = document.getElementById('webauthn-auth-form'); 5 | var f = function(e) { 6 | //console.log(e); 7 | e.preventDefault(); 8 | if (navigator.credentials) { 9 | var opts = JSON.parse(element.getAttribute("data-credential-options")); 10 | opts.challenge = unpack(opts.challenge); 11 | opts.allowCredentials.forEach(function(cred) { cred.id = unpack(cred.id); }); 12 | //console.log(opts); 13 | navigator.credentials.get({publicKey: opts}). 14 | then(function(cred){ 15 | //console.log(cred); 16 | //window.cred = cred 17 | 18 | var rawId = pack(cred.rawId); 19 | var authValue = { 20 | type: cred.type, 21 | id: rawId, 22 | rawId: rawId, 23 | response: { 24 | authenticatorData: pack(cred.response.authenticatorData), 25 | clientDataJSON: pack(cred.response.clientDataJSON), 26 | signature: pack(cred.response.signature) 27 | } 28 | }; 29 | 30 | if (cred.response.userHandle) { 31 | authValue.response.userHandle = pack(cred.response.userHandle); 32 | } 33 | 34 | document.getElementById('webauthn-auth').value = JSON.stringify(authValue); 35 | element.removeEventListener("submit", f); 36 | element.submit(); 37 | }). 38 | catch(function(e){document.getElementById('webauthn-auth-button').innerHTML = "Error authenticating using WebAuthn: " + e}); 39 | } else { 40 | document.getElementById('webauthn-auth-button').innerHTML = "WebAuthn not supported by browser, or browser has disabled it on this page"; 41 | } 42 | }; 43 | element.addEventListener("submit", f); 44 | })(); 45 | 46 | -------------------------------------------------------------------------------- /doc/release_notes/2.41.0.txt: -------------------------------------------------------------------------------- 1 | = Improvements 2 | 3 | * When making a change to an account (e.g. changing a login), tokens 4 | for the account are now cleared or reset. Previously, if you 5 | requested a password reset, then requested a login change, and then 6 | changed the login, the password reset link would still be valid 7 | after the login change was made, until the password reset token 8 | expired (default: 1 day). If the reason you are chaging your login 9 | is that you suspect your email may be compromised, you probably 10 | wouldn't want the reset password link to still be valid after the 11 | login change. 12 | 13 | The following account changes trigger clearing of tokens: 14 | 15 | * change login 16 | * close account 17 | * reset password 18 | * unlock account 19 | * verify account 20 | 21 | The following account tokens are cleared upon such changes: 22 | 23 | * active sessions (other than logged in session) 24 | * email auth 25 | * jwt refresh (if not logged in) 26 | * lockout (updates token if it exists) 27 | * remember (creates and uses new remember token if logged in via 28 | remember token) 29 | * reset password 30 | * single session (if not logged in) 31 | * verify account 32 | * verify login change 33 | 34 | This is a more secure default, and it is expected that it will 35 | not negatively affect the vast majority of Rodauth installations. 36 | However, due to Rodauth's very configurable nature, it is possible 37 | it will cause issues for some installations. 38 | 39 | = Backwards Compatibility 40 | 41 | * If clearing tokens on account change causes problems for your 42 | application, you can revert to clearing tokens only on account 43 | close: 44 | 45 | clear_tokens do |reason| 46 | super(reason) if reason == :close_account 47 | end 48 | 49 | * If you were calling after_close_account directly to clear tokens, 50 | you should now also call: 51 | 52 | clear_tokens(:close_account) 53 | 54 | As some token clearing now occurs in clear_tokens and not in 55 | after_close_account. 56 | -------------------------------------------------------------------------------- /doc/release_notes/2.26.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An argon2_secret configuration method has been added to the argon2 4 | feature, supporting argon2's built-in password peppering. 5 | 6 | = Other Improvements 7 | 8 | * Links are no longer automatically displayed for routes that are 9 | disabled by calling the *_route method with nil. 10 | 11 | * The QR code used by the otp feature now uses a white background 12 | instead of a transparent background, fixing issues when the 13 | underlying background is dark. 14 | 15 | * Input parameter bytesize is now limited to 1024 bytes by default. 16 | Parameters larger than that will be ignored, as if they weren't 17 | submitted. 18 | 19 | * The Rodauth::Auth class for internal request classes now uses the 20 | same configuration name as the class it is based on. 21 | 22 | * The session_key_prefix configuration method no longer also prefixes 23 | the keys used in the flash hash. 24 | 25 | * The *_path and *_url methods now return nil when the related *_route 26 | method returns nil, indicating the route is disabled. 27 | 28 | * A more explicit error message is raised when using a feature that 29 | requires the hmac_secret being set and not setting hmac_secret. 30 | 31 | = Backwards Compatibility 32 | 33 | * If you are using session_key_prefix and flash messages, you will 34 | probably need to adjust your code to remove the prefix from the 35 | expected flash keys, or manually prefix the flash keys by using 36 | the flash_error_key and flash_notice_key configuration methods. 37 | 38 | * The limiting of input parameter bytesizes by default could potentially 39 | break applications that use Rodauth's parameter parsing method to 40 | handle parameters that Rodauth itself doesn't handle. You can use 41 | the max_param_bytesize configuration method to set a larger bytesize, 42 | or use a value of nil with the method for the previous behavior of 43 | no limit. Additionally, to customize the behavior if a parameter 44 | is over the allowed bytesize, you can use the 45 | over_max_bytesize_param_value configuration method. 46 | -------------------------------------------------------------------------------- /doc/create_account.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for Create Account Feature 2 | 3 | The create account feature allows users to create new accounts. 4 | 5 | == Auth Value Methods 6 | 7 | create_account_additional_form_tags :: HTML fragment containing additional form tags to use on the create account form. 8 | create_account_button :: The text to use for the create account button. 9 | create_account_error_flash :: The flash error to show for unsuccessful account creation. 10 | create_account_notice_flash :: The flash notice to show after successful account creation. 11 | create_account_page_title :: The page title to use on the create account form. 12 | create_account_redirect :: Where to redirect after creating the account. 13 | create_account_route :: The route to the create account action. Defaults to +create-account+. 14 | create_account_set_password? :: Whether to ask for a password to be set on the create account form. Defaults to true if not verifying accounts. If set to false, an alternative method to set the password should be used (assuming you want to allow password authentication). 15 | 16 | == Auth Methods 17 | 18 | after_create_account :: Run arbitrary code after creating the account. 19 | before_create_account :: Run arbitrary code before creating the account. 20 | before_create_account_route :: Run arbitrary code before handling a create account route. 21 | create_account_autologin? :: Whether to autologin the user upon successful account creation, true by default unless verifying accounts. 22 | create_account_link_text :: The text to use for a link to the create account form. 23 | create_account_response :: Return a response after successful account creation. By default, redirects to +create_account_redirect+. 24 | create_account_view :: The HTML to use for the create account form. 25 | new_account(login) :: Instantiate a new account hash for the given login, without saving it. 26 | save_account :: Insert the account into the database, or return nil/false if that was not successful. 27 | set_new_account_password :: Set the password for a new account if +account_password_hash_column+ is set, without saving. 28 | -------------------------------------------------------------------------------- /lib/rodauth/features/webauthn_autofill.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:webauthn_autofill, :WebauthnAutofill) do 5 | depends :webauthn_login 6 | 7 | auth_value_method :webauthn_autofill?, true 8 | auth_value_method :webauthn_autofill_js, File.binread(File.expand_path('../../../../javascript/webauthn_autofill.js', __FILE__)).freeze 9 | 10 | translatable_method :webauthn_invalid_webauthn_id_message, "no webauthn key with given id found" 11 | 12 | route(:webauthn_autofill_js) do |r| 13 | before_webauthn_autofill_js_route 14 | r.get do 15 | set_response_header('content-type', 'text/javascript') 16 | webauthn_autofill_js 17 | end 18 | end 19 | 20 | def webauthn_allow 21 | return [] unless logged_in? || account 22 | super 23 | end 24 | 25 | def webauthn_user_verification 26 | 'preferred' 27 | end 28 | 29 | def webauthn_authenticator_selection 30 | super.merge({ 'residentKey' => 'required', 'requireResidentKey' => true }) 31 | end 32 | 33 | def login_field_autocomplete_value 34 | request.path_info == login_path ? "#{super} webauthn" : super 35 | end 36 | 37 | private 38 | 39 | def _login_form_footer 40 | footer = super 41 | footer += render("webauthn-autofill") if webauthn_autofill? && !valid_login_entered? 42 | footer 43 | end 44 | 45 | def account_from_webauthn_login 46 | return super if param_or_nil(login_param) 47 | 48 | credential_id = webauthn_auth_data["id"] 49 | account_id = db[webauthn_keys_table] 50 | .where(webauthn_keys_webauthn_id_column => credential_id) 51 | .get(webauthn_keys_account_id_column) 52 | 53 | unless account_id 54 | throw_error_reason(:invalid_webauthn_id, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_webauthn_id_message) 55 | end 56 | 57 | account_from_id(account_id) 58 | end 59 | 60 | def webauthn_login_options? 61 | return true unless param_or_nil(login_param) 62 | super 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/rodauth/features/email_base.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:email_base, :EmailBase) do 5 | translatable_method :email_subject_prefix, '' 6 | auth_value_method :require_mail?, true 7 | auth_value_method :allow_raw_email_token?, false 8 | 9 | redirect :default_post_email 10 | 11 | auth_value_methods( 12 | :email_from 13 | ) 14 | 15 | auth_methods( 16 | :create_email, 17 | :email_to, 18 | :send_email 19 | ) 20 | 21 | def post_configure 22 | super 23 | require 'mail' if require_mail? 24 | end 25 | 26 | def email_from 27 | "webmaster@#{domain}" 28 | end 29 | 30 | def email_to 31 | account[login_column] 32 | end 33 | 34 | private 35 | 36 | def send_email(email) 37 | email.deliver! 38 | end 39 | 40 | def create_email(subject, body) 41 | create_email_to(email_to, subject, body) 42 | end 43 | 44 | def create_email_to(to, subject, body) 45 | m = Mail.new 46 | m.from = email_from 47 | m.to = to 48 | m.subject = "#{email_subject_prefix}#{subject}" 49 | m.body = body 50 | m 51 | end 52 | 53 | def token_link(route, param, key) 54 | route_url(route, param => token_param_value(key)) 55 | end 56 | 57 | def token_param_value(key) 58 | "#{account_id}#{token_separator}#{convert_email_token_key(key)}" 59 | end 60 | 61 | def convert_email_token_key(key) 62 | convert_token_key(key) 63 | end 64 | 65 | def account_from_key(token, status_id=nil) 66 | id, key = split_token(token) 67 | id = convert_token_id(id) 68 | return unless id && key 69 | 70 | return unless actual = yield(id) 71 | 72 | unless (hmac_secret && timing_safe_eql?(key, convert_email_token_key(actual))) || 73 | (hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual))) || 74 | ((!hmac_secret || allow_raw_email_token?) && timing_safe_eql?(key, actual)) 75 | return 76 | end 77 | _account_from_id(id, status_id) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /demo-site/views/index.erb: -------------------------------------------------------------------------------- 1 | <% if rodauth.logged_in? %> 2 |

You are logged in as <%= DB[:accounts].where(:id=>rodauth.session_value).get(:email) %>

3 | <% if rodauth.two_factor_authenticated? %> 4 |

You have authenticated using multifactor authentication (<%= rodauth.authenticated_by.join(' and ') %>).

5 | <% elsif rodauth.uses_two_factor_authentication? %> 6 |

You have logged in via single factor authentication (<%= rodauth.authenticated_by.first %>), but have not authenticated using multifactor authentication.

7 | <% else %> 8 |

You have authenticated via <%= rodauth.authenticated_by.first %>

9 | <% end %> 10 | 11 | 25 | <% else %> 26 |

Overview

27 | 28 |

This is the demo site for Rodauth. Rodauth is an authentication and account management framework for rack applications, with the following design goals:

29 | 30 |
    31 |
  1. Security: Ship in a maximum security by default configuration
  2. 32 |
  3. Simplicity: Allow for easy configuration via a DSL
  4. 33 |
  5. Flexibility: Allow for easy overriding of any part of the framework
  6. 34 |
35 | 36 |

It's easiest to get started by going to the login page.

37 | 38 |

This demo site is part of the Rodauth repository, so if you want to know how it works, you can review the source.

39 | <% end %> 40 | -------------------------------------------------------------------------------- /doc/otp_lockout_email.rdoc: -------------------------------------------------------------------------------- 1 | = Documentation for OTP Lockout Email Feature 2 | 3 | The otp_lockout_email feature emails users when: 4 | 5 | * TOTP authentication is locked out 6 | * TOTP authentication is unlocked 7 | * A TOTP unlock attempt has failed 8 | 9 | The otp_unlock_email feature depends on the otp_lockout and email_base features. 10 | 11 | == Auth Value Methods 12 | 13 | otp_locked_out_email_body :: Body to use for the email notifying user that TOTP authentication has been locked out. 14 | otp_locked_out_email_subject :: Subject to use for the email notifying user that TOTP authentication has been locked out. 15 | otp_unlock_failed_email_body :: Body to use for the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication. 16 | otp_unlock_failed_email_subject :: Subject to use for the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication. 17 | otp_unlocked_email_body :: Body to use for the email notifying user that TOTP authentication has been unlocked. 18 | otp_unlocked_email_subject :: Subject to use for the email notifying user that TOTP authentication has been unlocked. 19 | send_otp_locked_out_email? :: Whether to send an email when TOTP authentication is locked out. 20 | send_otp_unlock_failed_email? :: Whether to send an email when there has been an unsuccessful attempt to unlock TOTP authentication. 21 | send_otp_unlocked_email? :: Whether to send an email when TOTP authentication is unlocked. 22 | 23 | == Auth Methods 24 | 25 | create_otp_locked_out_email :: A Mail::Message for the email notifying user that TOTP authentication has been locked out. 26 | create_otp_unlock_failed_email :: A Mail::Message for the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication. 27 | create_otp_unlocked_email :: A Mail::Message for the email notifying user that TOTP authentication has been unlocked. 28 | send_otp_locked_out_email :: Send the email notifying user that TOTP authentication has been locked out. 29 | send_otp_unlock_failed_email :: Send the email notifying user that there has been an unsuccessful attempt to unlock TOTP authentication. 30 | send_otp_unlocked_email :: Send the email notifying user that TOTP authentication has been unlocked. 31 | -------------------------------------------------------------------------------- /lib/rodauth/features/http_basic_auth.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | module Rodauth 4 | Feature.define(:http_basic_auth, :HttpBasicAuth) do 5 | auth_value_method :http_basic_auth_realm, "protected" 6 | auth_value_method :require_http_basic_auth?, false 7 | 8 | def logged_in? 9 | ret = super 10 | 11 | if !ret && !defined?(@checked_http_basic_auth) 12 | http_basic_auth 13 | ret = super 14 | end 15 | 16 | ret 17 | end 18 | 19 | def require_login 20 | if require_http_basic_auth? 21 | require_http_basic_auth 22 | end 23 | 24 | super 25 | end 26 | 27 | def require_http_basic_auth 28 | unless http_basic_auth 29 | set_http_basic_auth_error_response 30 | return_response 31 | end 32 | end 33 | 34 | def http_basic_auth 35 | return @checked_http_basic_auth if defined?(@checked_http_basic_auth) 36 | 37 | @checked_http_basic_auth = nil 38 | return unless token = ((v = request.env['HTTP_AUTHORIZATION']) && v[/\A *Basic (.*)\Z/, 1]) 39 | 40 | username, password = token.unpack("m*").first.split(/:/, 2) 41 | return unless username && password 42 | 43 | catch_error do 44 | unless account_from_login(username) 45 | throw_basic_auth_error(login_param, no_matching_login_message) 46 | end 47 | 48 | before_login_attempt 49 | 50 | unless open_account? 51 | throw_basic_auth_error(login_param, no_matching_login_message) 52 | end 53 | 54 | unless password_match?(password) 55 | after_login_failure 56 | throw_basic_auth_error(password_param, invalid_password_message) 57 | end 58 | 59 | transaction do 60 | before_login 61 | login_session('password') 62 | after_login 63 | end 64 | 65 | @checked_http_basic_auth = true 66 | return true 67 | end 68 | 69 | nil 70 | end 71 | 72 | private 73 | 74 | def set_http_basic_auth_error_response 75 | response.status = 401 76 | set_response_header("www-authenticate", "Basic realm=\"#{http_basic_auth_realm}\"") 77 | end 78 | 79 | def throw_basic_auth_error(*args) 80 | set_http_basic_auth_error_response 81 | throw_error(*args) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /doc/release_notes/2.15.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An internal_request feature has been added. This feature allows 4 | for interacting with Rodauth by calling methods, instead of having 5 | to use a website or JSON API. This feature is designed primarily 6 | for administrative use, so that administrators can create accounts, 7 | change passwords or logins for accounts, and handle similar actions 8 | without the user of the account being involved. 9 | 10 | For example, assuming you've loaded the change_password and 11 | internal_request features, and that your Roda class that 12 | is loading Rodauth is named App, you can change the password 13 | for the account with id 1 using: 14 | 15 | App.rodauth.change_password(account_id: 1, password: 'foobar') 16 | 17 | The internal request methods are implemented as class methods 18 | on the Rodauth::Auth subclass (the object returned by App.rodauth). 19 | These methods call methods on a subclass of that class specific 20 | to internal requests. 21 | 22 | The reason the feature is named internal_request is that these 23 | methods are implemented by submitting a request internally, that is 24 | processed almost exactly the same way as Rodauth would process a 25 | web request. 26 | 27 | See the internal_request feature documentation for details on which 28 | internal request methods are available and the options they take. 29 | 30 | * A path_class_methods feature has been added, that allows for calling 31 | *_path and *_url as class methods. If you would like to call the 32 | *_url methods as class methods, make sure to use the base_url 33 | configuration method to set the base URL so that it does not require 34 | request-specific information. 35 | 36 | * Rodauth::Auth classes now have a configuration_name method that 37 | returns the configuration name associated with the class. They also 38 | have a configuration method that returns the configuration 39 | associated with the class. 40 | 41 | * Rodauth::Feature now supports an internal_request_method method for 42 | specifying which methods are supported as internal request methods. 43 | 44 | = Other Improvements 45 | 46 | * The default base_url configuration method will now use the domain 47 | method to get the domain to use, instead of getting the domain 48 | information directly from the request environment. 49 | -------------------------------------------------------------------------------- /doc/release_notes/2.10.0.txt: -------------------------------------------------------------------------------- 1 | = New Features 2 | 3 | * An argon2 feature has been added that supports using the argon2 4 | password hashing algorithm instead of the bcrypt password hashing 5 | algorithm. While argon2 does not provide an advantage over bcrypt 6 | if the attacker cannot access the password hashes directly (which 7 | is how Rodauth is recommended to be used), in cases where attackers 8 | can access the password hashes directly, argon2 is thought to be 9 | more difficult or expensive to crack due to requiring more memory 10 | (bcrypt is not a memory-hard password hash algorithm). 11 | 12 | If you are using this feature with Rodauth's database authentication 13 | functions, you need to make sure that the database authentication 14 | functions are configured to support argon2 in addition to bcrypt. 15 | You can do this by passing the :argon2 option when calling the 16 | method to define the database functions. In this example, DB should 17 | be your Sequel::Database object (this could be self if used in a 18 | Sequel migration): 19 | 20 | require 'rodauth/migrations' 21 | 22 | # If the functions are already defined and you are not using PostgreSQL, 23 | # you need to drop the existing functions. 24 | Rodauth.drop_database_authentication_functions(DB) 25 | 26 | # If you are using the disallow_password_reuse feature, also drop the 27 | # database functions related to that if you are not using PostgreSQL: 28 | Rodauth.drop_database_previous_password_check_functions(DB) 29 | 30 | # Define new functions that support argon2: 31 | Rodauth.create_database_authentication_functions(DB, argon2: true) 32 | 33 | # If you are using the disallow_password_reuse feature, also define 34 | # new functions that support argon2 for that: 35 | Rodauth.create_database_previous_password_check_functions(DB, argon2: true) 36 | 37 | You can transparently migrate bcrypt password hashes to argon2 38 | password hashes whenever a user successfully uses their password 39 | by using the argon2 feature in combination with the 40 | update_password_hash feature. 41 | 42 | = Other Improvements 43 | 44 | * Unnecessary queries to determine whether the new password matches 45 | a previous password are now skipped when using the create_account 46 | or verify_account features with the disallow_password_reuse 47 | feature. 48 | --------------------------------------------------------------------------------