rodauth 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +18 -0
  3. data/doc/guides/admin_activation.rdoc +46 -0
  4. data/doc/guides/already_authenticated.rdoc +10 -0
  5. data/doc/guides/alternative_login.rdoc +46 -0
  6. data/doc/guides/create_account_programmatically.rdoc +38 -0
  7. data/doc/guides/delay_password.rdoc +25 -0
  8. data/doc/guides/email_only.rdoc +16 -0
  9. data/doc/guides/i18n.rdoc +26 -0
  10. data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
  11. data/doc/guides/links.rdoc +12 -0
  12. data/doc/guides/login_return.rdoc +37 -0
  13. data/doc/guides/password_column.rdoc +25 -0
  14. data/doc/guides/password_confirmation.rdoc +37 -0
  15. data/doc/guides/password_requirements.rdoc +30 -0
  16. data/doc/guides/paths.rdoc +36 -0
  17. data/doc/guides/query_params.rdoc +9 -0
  18. data/doc/guides/redirects.rdoc +17 -0
  19. data/doc/guides/registration_field.rdoc +68 -0
  20. data/doc/guides/require_mfa.rdoc +30 -0
  21. data/doc/guides/reset_password_autologin.rdoc +21 -0
  22. data/doc/guides/status_column.rdoc +28 -0
  23. data/doc/guides/totp_or_recovery.rdoc +16 -0
  24. data/doc/jwt_refresh.rdoc +6 -0
  25. data/doc/otp.rdoc +1 -0
  26. data/doc/release_notes/2.2.0.txt +39 -0
  27. data/lib/rodauth.rb +4 -4
  28. data/lib/rodauth/features/active_sessions.rb +5 -7
  29. data/lib/rodauth/features/audit_logging.rb +2 -0
  30. data/lib/rodauth/features/email_auth.rb +1 -1
  31. data/lib/rodauth/features/jwt.rb +5 -6
  32. data/lib/rodauth/features/jwt_cors.rb +15 -15
  33. data/lib/rodauth/features/jwt_refresh.rb +29 -6
  34. data/lib/rodauth/features/otp.rb +5 -1
  35. data/lib/rodauth/features/password_complexity.rb +4 -2
  36. data/lib/rodauth/features/single_session.rb +1 -1
  37. data/lib/rodauth/features/sms_codes.rb +0 -1
  38. data/lib/rodauth/features/two_factor_base.rb +4 -4
  39. data/lib/rodauth/features/verify_account.rb +4 -0
  40. data/lib/rodauth/features/verify_account_grace_period.rb +2 -4
  41. data/lib/rodauth/features/webauthn.rb +1 -3
  42. data/lib/rodauth/version.rb +1 -1
  43. metadata +25 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11417c5f63db5803f35ba8a92e98827b9f0deeb0a774071b5fbe712fa24f02fd
4
- data.tar.gz: b0ee20c0ce7af6eea42b32bcab2e476a2ee75b62b283281de7001d5ba4002597
3
+ metadata.gz: f672a0312d4a0c6103bb763d339161119551a0e64cc248546a0cb52f4e6784c1
4
+ data.tar.gz: 530d322cc3ddba655959238aafe155512de11f090afb9465980181dcc729d1b9
5
5
  SHA512:
6
- metadata.gz: 13110a40f40a08c7fb03f1ce44a723afc6e53b4f4b16f690c852dc0adfc4d1bb08b6b87f164be98d50dfc61a6c99a35759901ed3bc7b5ab3dbbcb48b00b6ec5e
7
- data.tar.gz: 2d7f46d8a9370b67af58eb60d5ac8f324f2619d3cd9bac10b6a27890c9637ceff229ce0404a6d00c9c7fdf8d796d045d3bd06e7489fe08f589580d6a20271cf4
6
+ metadata.gz: d8018f5e02d077bd8625c17c8c5a1f607b986c1916296132ef68e2651d2af26d65ddbdfdc9b5f1352431c74caa30e94d270cc03840207303f8fa0e6a44910a0b
7
+ data.tar.gz: 5f24ce4d800d3bbe914dac40dba1cd68b78af4aff5efdaf976b32702d45b5fe7809453d2a650f407437afb1b9d6206ff3d79e6f703fcd85d8d55a0f0658464eb
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ === 2.2.0 (2020-07-20)
2
+
3
+ * Allow removing all jwt_refresh tokens when logging out by providing a value of "all" as the token to remove (jeremyevans)
4
+
5
+ * Allow removing specific jwt_refresh token when logging out by providing the token to remove (jeremyevans)
6
+
7
+ * Avoid NoMethodError when checking if session is authenticated when using two factor auth, verify_account_grace_period, and email_auth (jeremyevans) (#105)
8
+
9
+ * Reduce queries in #authenticated? and #require_authentication when using two factor authentication (janko) (#106)
10
+
11
+ * Treat verify_account_email_resend returning false as an error in the verify_account feature (jeremyevans)
12
+
13
+ * Fix use of password_dictionary configuration method in password_complexity feature (jeremyevans)
14
+
15
+ * Remove unnecessary conditionals (jeremyevans)
16
+
17
+ * Add otp_last_use to the otp feature, returning the time of last successful OTP use (jeremyevans) (#103)
18
+
1
19
  === 2.1.0 (2020-06-09)
2
20
 
3
21
  * Do not check CSRF tokens by default for requests using JWT (janko, jeremyevans) (#99)
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,46 @@
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 colummn 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
@@ -0,0 +1,38 @@
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
+ )
@@ -0,0 +1,25 @@
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 remebering 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
@@ -0,0 +1,16 @@
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.
@@ -0,0 +1,26 @@
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"
@@ -0,0 +1,12 @@
1
+ = Display authentication links
2
+
3
+ You can retrieve a relative URL to any Rodauth action by calling the
4
+ corresponding <tt>*_path</tt> method on the Rodauth instance:
5
+
6
+ <a href="<%= rodauth.login_path %>">Sign in</a>
7
+ <a href="<%= rodauth.create_account_path %>">Sign up</a>
8
+
9
+ For absolute URLs instead of paths, you can use the <tt>*_url</tt> methods:
10
+
11
+ <a href="<%= rodauth.login_url %>">Sign in</a>
12
+ <a href="<%= rodauth.create_account_url %>">Sign up</a>
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,30 @@
1
+ = Customize password requirements
2
+
3
+ By default, Rodauth requires passwords to have at least 6 characters. You can
4
+ modify the minimum 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
+ end
12
+
13
+ You can use the {disallow common passwords feature}[rdoc-ref:doc/disallow_common_passwords.rdoc]
14
+ to prevent the usage of common passwords (the most common 10,000 by default).
15
+
16
+ You can use additional complexity checks on passwords via the {password
17
+ complexity feature}[rdoc-ref:doc/password_complexity.rdoc], though most of
18
+ those complexity checks are no longer considered modern security best
19
+ practices and are likely to decrease overall security.
20
+
21
+ If you want complete control over whether passwords meet requirements, you
22
+ can use the <tt>password_meets_requirements?</tt> configuration method.
23
+
24
+ plugin :rodauth do
25
+ enable :login, :logout, :create_account
26
+
27
+ password_meets_requirements? do |password|
28
+ #true if password meets requirements, false otherwise
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ = Change route path
2
+
3
+ You can change the URL path of any Rodauth route by overriding the
4
+ corresponding <tt>*_route</tt> 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 create account route to "/register"
13
+ create_account_route "register"
14
+
15
+ # Change password reset request route to "/reset-password/request"
16
+ reset_password_request_route "reset-password/request"
17
+ end
18
+
19
+ If you want to add a prefix to all Rodauth routes, you should use the +prefix+
20
+ setting:
21
+
22
+ plugin :rodauth do
23
+ enable :login, :logout
24
+
25
+ # Use /auth prefix to each Rodauth route
26
+ prefix "/auth"
27
+ end
28
+
29
+ route do |r|
30
+ r.on "auth" do
31
+ # Serve Rodauth routes under the /auth branch of the routing tree
32
+ r.rodauth
33
+ end
34
+
35
+ # ...
36
+ end
@@ -0,0 +1,9 @@
1
+ = Pass query parameters to auth URLs
2
+
3
+ The <tt>*_path</tt> and <tt>*_url</tt> 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"
@@ -0,0 +1,17 @@
1
+ = Change redirect destination
2
+
3
+ You can change the redirect destination for any Rodauth action by overriding
4
+ the corresponding <tt>*_redirect</tt> 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
@@ -0,0 +1,68 @@
1
+ = Add new field during account creation
2
+
3
+ The create account form only handles login and password parameters by
4
+ default. However, you might want to ask for additional information during
5
+ account creation, such as requiring the user to also enter their full name
6
+ or their company's name.
7
+
8
+ == A) Accounts table
9
+
10
+ Let's assume you wanted to wanted to store the additional field(s) directly on
11
+ the +accounts+ table:
12
+
13
+ atler_table :accounts do
14
+ add_column :name, String
15
+ end
16
+
17
+ You need to override the <tt>create-account</tt> template, which by default in
18
+ Rodauth you can do by adding a <tt>create-account.erb</tt> template in your
19
+ Roda +views+ directory.
20
+
21
+ Once you've added the <tt>create-account.erb</tt> template, and had it include
22
+ a field for the +name+, you can handle the submission of that field in a before
23
+ create account hook:
24
+
25
+ plugin :rodauth do
26
+ enable :login, :logout, :create_account
27
+
28
+ before_create_account do
29
+ # Validate presence of the name field
30
+ unless name = param_or_nil("name")
31
+ throw_error_status(422, "name", "must be present")
32
+ end
33
+
34
+ # Assign the new field to the account record
35
+ account[:name] = name
36
+ end
37
+ end
38
+
39
+ == B) Separate table
40
+
41
+ Alternatively, you can store the additional field(s) in separate table, for
42
+ example:
43
+
44
+ create_table :account_names do
45
+ foreign_key :account_id, :accounts, primary_key: true, type: :Bignum
46
+ String :name, null: false
47
+ end
48
+
49
+ You can then handle the new submitted field as follows:
50
+
51
+ plugin :rodauth do
52
+ enable :login, :logout, :create_account
53
+
54
+ before_create_account do
55
+ # Validate presence of the name field
56
+ throw_error_status(422, "name", "must be present") unless param_or_nil("name")
57
+ end
58
+
59
+ after_create_account do
60
+ # Create the associated record
61
+ db[:account_names].insert(account_id: account[:id], name: param("name"))
62
+ end
63
+
64
+ after_close_account do
65
+ # Delete the associated record
66
+ db[:account_names].where(account_id: account[:id]).delete
67
+ end
68
+ end
@@ -0,0 +1,30 @@
1
+ = Require multifactor authentication after login
2
+
3
+ You may want to require multifactor authentication on login for people
4
+ that have multifactor authentication set up. The +require_authentication+
5
+ Rodauth method works for pages that require an authenticated user, but not for
6
+ pages where authentication is optional.
7
+
8
+ You can set this up as follows:
9
+
10
+ plugin :rodauth do
11
+ enable :login, :logout, :otp
12
+
13
+ # If you don't want to show an error message when redirecting
14
+ # to the multifactor authentication page.
15
+ two_factor_need_authentication_error_flash nil
16
+
17
+ # Display the same flash message after multifactor
18
+ # authentication than is displayed after login
19
+ two_factor_auth_notice_flash { login_notice_flash }
20
+ end
21
+
22
+ route do |r|
23
+ r.rodauth
24
+
25
+ if rodauth.logged_in? && rodauth.two_factor_authentication_setup?
26
+ rodauth.require_two_factor_authenticated
27
+ end
28
+
29
+ # ...
30
+ end
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,28 @@
1
+ = Store account status in a text column
2
+
3
+ By default, Rodauth recommends using a separate table for account statuses, and
4
+ linking them via foreign keys. This is useful as it achieves an enum-like
5
+ behaviour, where the database ensures a constrained set of status values.
6
+
7
+ However, if you use a testing environment that starts with a blank database,
8
+ and don't want to fix your testing environment to support real foreign keys,
9
+ you can configure Rodauth to store the account status in a text column.
10
+ Doing so results in problems if a text value you do not expect gets stored
11
+ in the column. We can mitigate the problems by using a CHECK constraint
12
+ on the column.
13
+
14
+ create_table :accounts do
15
+ # ...
16
+ String :status, null: false, default: "verified",
17
+ check: {status: %w'unverified verified closed'}
18
+ end
19
+
20
+ Then we can configure Rodauth to support this.
21
+
22
+ plugin :rodauth do
23
+ # ...
24
+ account_status_column :status
25
+ account_unverified_status_value "unverified"
26
+ account_open_status_value "verified"
27
+ account_closed_status_value "closed"
28
+ end
@@ -0,0 +1,16 @@
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
@@ -13,6 +13,12 @@ older access tokens. Older access tokens remain valid until they expire. You
13
13
  can use the active_sessions feature if you want previous access tokens to be invalid
14
14
  as soon as the refresh token is used.
15
15
 
16
+ You can have multiple active refresh tokens active at a time, since each browser session
17
+ will generally use a separate refresh token. If you would like to revoke a refresh token
18
+ when logging out, provide the refresh token when submitting the JSON request to logout.
19
+ If you would like to remove all refresh tokens for the account when logging out, provide
20
+ a value of <tt>all</tt> as the token value.
21
+
16
22
  This feature depends on the jwt feature.
17
23
 
18
24
  == Auth Value Methods
@@ -73,6 +73,7 @@ otp_auth_view :: The HTML to use for the OTP authentication form.
73
73
  otp_disable_view :: The HTML to use for the OTP disable form.
74
74
  otp_exists? :: Whether the current account has setup OTP.
75
75
  otp_key :: The stored OTP secret for the account.
76
+ otp_last_use :: The last time OTP authentication was successful for the account.
76
77
  otp_locked_out? :: Whether the current account has been locked out of OTP authentication.
77
78
  otp_new_secret :: A new secret to use when setting up OTP.
78
79
  otp_provisioning_name :: The provisioning name to use during OTP setup, defaults to the account's email.
@@ -0,0 +1,39 @@
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.
@@ -137,7 +137,9 @@ module Rodauth
137
137
  feature.module_eval(&block)
138
138
  configuration.def_configuration_methods(feature)
139
139
 
140
+ # :nocov:
140
141
  if constant
142
+ # :nocov:
141
143
  Rodauth.const_set(constant, feature)
142
144
  Rodauth::FeatureConfiguration.const_set(constant, configuration)
143
145
  end
@@ -336,10 +338,8 @@ module Rodauth
336
338
  end
337
339
 
338
340
  def freeze
339
- if opts[:rodauths]
340
- opts[:rodauths].each_value(&:freeze)
341
- opts[:rodauths].freeze
342
- end
341
+ opts[:rodauths].each_value(&:freeze)
342
+ opts[:rodauths].freeze
343
343
  super
344
344
  end
345
345
  end
@@ -118,14 +118,12 @@ module Rodauth
118
118
  end
119
119
 
120
120
  def before_logout
121
- if request.post?
122
- if param_or_nil(global_logout_param)
123
- remove_all_active_sessions
124
- else
125
- remove_current_session
126
- end
121
+ if param_or_nil(global_logout_param)
122
+ remove_all_active_sessions
123
+ else
124
+ remove_current_session
127
125
  end
128
- super if defined?(super)
126
+ super
129
127
  end
130
128
 
131
129
  def session_inactivity_deadline_condition
@@ -82,7 +82,9 @@ module Rodauth
82
82
 
83
83
  def audit_log_ds
84
84
  ds = db[audit_logging_table]
85
+ # :nocov:
85
86
  if db.database_type == :postgres
87
+ # :nocov:
86
88
  # For PostgreSQL, use RETURNING NULL. This allows the feature
87
89
  # to be used with INSERT but not SELECT permissions on the
88
90
  # table, useful for audit logging where the database user
@@ -215,7 +215,7 @@ module Rodauth
215
215
  # that allows login access to the account becomes a
216
216
  # security liability, and it is best to remove it.
217
217
  remove_email_auth_key
218
- super if defined?(super)
218
+ super
219
219
  end
220
220
 
221
221
  def after_close_account
@@ -138,6 +138,11 @@ module Rodauth
138
138
  !!(jwt_token && jwt_payload)
139
139
  end
140
140
 
141
+ def view(page, title)
142
+ return super unless use_jwt?
143
+ return_json_response
144
+ end
145
+
141
146
  private
142
147
 
143
148
  def check_csrf?
@@ -256,12 +261,6 @@ module Rodauth
256
261
  @json_response ||= {}
257
262
  end
258
263
 
259
- def _view(meth, page)
260
- return super unless use_jwt?
261
- return super if meth == :render
262
- return_json_response
263
- end
264
-
265
264
  def _json_response_body(hash)
266
265
  request.send(:convert_to_json, hash)
267
266
  end
@@ -13,27 +13,27 @@ module Rodauth
13
13
  auth_methods(:jwt_cors_allow?)
14
14
 
15
15
  def jwt_cors_allow?
16
- if origin = request.env['HTTP_ORIGIN']
17
- case allowed = jwt_cors_allow_origin
18
- when String
19
- timing_safe_eql?(origin, allowed)
20
- when Array
21
- allowed.any?{|s| timing_safe_eql?(origin, s)}
22
- when Regexp
23
- allowed =~ origin
24
- when true
25
- true
26
- else
27
- false
28
- end
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
29
  end
30
30
  end
31
31
 
32
32
  private
33
33
 
34
34
  def before_rodauth
35
- if (origin = request.env['HTTP_ORIGIN']) && jwt_cors_allow?
36
- response['Access-Control-Allow-Origin'] = origin
35
+ if jwt_cors_allow?
36
+ response['Access-Control-Allow-Origin'] = request.env['HTTP_ORIGIN']
37
37
 
38
38
  # Handle CORS preflight request
39
39
  if request.request_method == 'OPTIONS'
@@ -80,14 +80,10 @@ module Rodauth
80
80
  private
81
81
 
82
82
  def _account_from_refresh_token(token)
83
- id, token = split_token(token)
84
- return unless id && token
85
-
86
- token_id, key = split_token(token)
87
- return unless token_id && key
83
+ id, token_id, key = _account_refresh_token_split(token)
88
84
 
85
+ return unless key
89
86
  return unless actual = get_active_refresh_token(id, token_id)
90
-
91
87
  return unless timing_safe_eql?(key, convert_token_key(actual))
92
88
 
93
89
  ds = account_ds(id)
@@ -95,6 +91,16 @@ module Rodauth
95
91
  ds.first
96
92
  end
97
93
 
94
+ def _account_refresh_token_split(token)
95
+ id, token = split_token(token)
96
+ return unless id && token
97
+
98
+ token_id, key = split_token(token)
99
+ return unless token_id && key
100
+
101
+ [id, token_id, key]
102
+ end
103
+
98
104
  def get_active_refresh_token(account_id, token_id)
99
105
  jwt_refresh_token_account_ds(account_id).
100
106
  where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
@@ -134,6 +140,23 @@ module Rodauth
134
140
  hash
135
141
  end
136
142
 
143
+ def before_logout
144
+ if token = param_or_nil(jwt_refresh_token_key_param)
145
+ if token == 'all'
146
+ jwt_refresh_token_account_ds(session_value).delete
147
+ else
148
+ id, token_id, key = _account_refresh_token_split(token)
149
+
150
+ if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
151
+ jwt_refresh_token_account_ds(id).
152
+ where(jwt_refresh_token_id_column=>token_id).
153
+ delete
154
+ end
155
+ end
156
+ end
157
+ super if defined?(super)
158
+ end
159
+
137
160
  def after_close_account
138
161
  jwt_refresh_token_account_ds(account_id).delete
139
162
  super if defined?(super)
@@ -79,6 +79,7 @@ module Rodauth
79
79
  :otp,
80
80
  :otp_exists?,
81
81
  :otp_key,
82
+ :otp_last_use,
82
83
  :otp_locked_out?,
83
84
  :otp_new_secret,
84
85
  :otp_provisioning_name,
@@ -255,7 +256,6 @@ module Rodauth
255
256
  def otp_remove
256
257
  otp_key_ds.delete
257
258
  @otp_key = nil
258
- super if defined?(super)
259
259
  end
260
260
 
261
261
  def otp_add_key
@@ -269,6 +269,10 @@ module Rodauth
269
269
  update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
270
270
  end
271
271
 
272
+ def otp_last_use
273
+ convert_timestamp(otp_key_ds.get(otp_keys_last_use_column))
274
+ end
275
+
272
276
  def otp_record_authentication_failure
273
277
  otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
274
278
  end
@@ -26,14 +26,16 @@ module Rodauth
26
26
 
27
27
  def post_configure
28
28
  super
29
- return if singleton_methods.map(&:to_sym).include?(:password_dictionary)
29
+ return if method(:password_dictionary).owner != Rodauth::PasswordComplexity
30
30
 
31
31
  case password_dictionary_file
32
32
  when false
33
- return
33
+ # nothing
34
34
  when nil
35
35
  default_dictionary_file = '/usr/share/dict/words'
36
+ # :nocov:
36
37
  if File.file?(default_dictionary_file)
38
+ # :nocov:
37
39
  words = File.read(default_dictionary_file)
38
40
  end
39
41
  else
@@ -85,7 +85,7 @@ module Rodauth
85
85
  end
86
86
 
87
87
  def before_logout
88
- reset_single_session_key if request.post?
88
+ reset_single_session_key
89
89
  super if defined?(super)
90
90
  end
91
91
 
@@ -337,7 +337,6 @@ module Rodauth
337
337
  def sms_disable
338
338
  sms_ds.delete
339
339
  @sms = nil
340
- super if defined?(super)
341
340
  end
342
341
 
343
342
  def sms_confirm_failure
@@ -125,7 +125,7 @@ module Rodauth
125
125
  return true if two_factor_authenticated?
126
126
 
127
127
  # True if authenticated via single factor and 2nd factor not setup
128
- !two_factor_authentication_setup?
128
+ !uses_two_factor_authentication?
129
129
  end
130
130
 
131
131
  def require_authentication
@@ -134,7 +134,7 @@ module Rodauth
134
134
  # Avoid database query if already authenticated via 2nd factor
135
135
  return if two_factor_authenticated?
136
136
 
137
- require_two_factor_authenticated if two_factor_authentication_setup?
137
+ require_two_factor_authenticated if uses_two_factor_authentication?
138
138
  end
139
139
 
140
140
  def require_two_factor_setup
@@ -208,11 +208,11 @@ module Rodauth
208
208
  end
209
209
 
210
210
  def _two_factor_setup_links
211
- (super if defined?(super)) || []
211
+ []
212
212
  end
213
213
 
214
214
  def _two_factor_remove_links
215
- (super if defined?(super)) || []
215
+ []
216
216
  end
217
217
 
218
218
  def _two_factor_remove_all_from_session
@@ -70,6 +70,7 @@ module Rodauth
70
70
  end
71
71
 
72
72
  r.post do
73
+ verified = false
73
74
  if account_from_login(param(login_param)) && allow_resending_verify_account_email?
74
75
  if verify_account_email_recently_sent?
75
76
  set_redirect_error_flash verify_account_email_recently_sent_error_flash
@@ -79,8 +80,11 @@ module Rodauth
79
80
  before_verify_account_email_resend
80
81
  if verify_account_email_resend
81
82
  after_verify_account_email_resend
83
+ verified = true
82
84
  end
85
+ end
83
86
 
87
+ if verified
84
88
  set_notice_flash verify_account_email_sent_notice_flash
85
89
  else
86
90
  set_redirect_error_status(no_matching_login_error_status)
@@ -53,10 +53,7 @@ module Rodauth
53
53
  end
54
54
 
55
55
  def allow_email_auth?
56
- if defined?(super)
57
- return false unless super
58
- end
59
- !account_in_unverified_grace_period?
56
+ (defined?(super) ? super : true) && !account_in_unverified_grace_period?
60
57
  end
61
58
 
62
59
  def verify_account_check_already_logged_in
@@ -75,6 +72,7 @@ module Rodauth
75
72
  end
76
73
 
77
74
  def account_in_unverified_grace_period?
75
+ account || account_from_session
78
76
  account[account_status_column] == account_unverified_status_value &&
79
77
  verify_account_grace_period &&
80
78
  !verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
@@ -377,9 +377,7 @@ module Rodauth
377
377
  end
378
378
 
379
379
  def remove_webauthn_key(webauthn_id)
380
- ret = webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
381
- super if defined?(super)
382
- ret
380
+ webauthn_keys_ds.where(webauthn_keys_webauthn_id_column=>webauthn_id).delete == 1
383
381
  end
384
382
 
385
383
  def remove_all_webauthn_keys_and_user_ids
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 2
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 1
9
+ MINOR = 2
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-09 00:00:00.000000000 Z
11
+ date: 2020-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -247,7 +247,6 @@ extra_rdoc_files:
247
247
  - doc/http_basic_auth.rdoc
248
248
  - doc/create_account.rdoc
249
249
  - doc/email_base.rdoc
250
- - doc/internals.rdoc
251
250
  - doc/disallow_common_passwords.rdoc
252
251
  - doc/disallow_password_reuse.rdoc
253
252
  - doc/password_complexity.rdoc
@@ -304,6 +303,7 @@ extra_rdoc_files:
304
303
  - doc/release_notes/1.23.0.txt
305
304
  - doc/release_notes/2.0.0.txt
306
305
  - doc/release_notes/2.1.0.txt
306
+ - doc/release_notes/2.2.0.txt
307
307
  files:
308
308
  - CHANGELOG
309
309
  - MIT-LICENSE
@@ -323,8 +323,28 @@ files:
323
323
  - doc/disallow_password_reuse.rdoc
324
324
  - doc/email_auth.rdoc
325
325
  - doc/email_base.rdoc
326
+ - doc/guides/admin_activation.rdoc
327
+ - doc/guides/already_authenticated.rdoc
328
+ - doc/guides/alternative_login.rdoc
329
+ - doc/guides/create_account_programmatically.rdoc
330
+ - doc/guides/delay_password.rdoc
331
+ - doc/guides/email_only.rdoc
332
+ - doc/guides/i18n.rdoc
333
+ - doc/guides/internals.rdoc
334
+ - doc/guides/links.rdoc
335
+ - doc/guides/login_return.rdoc
336
+ - doc/guides/password_column.rdoc
337
+ - doc/guides/password_confirmation.rdoc
338
+ - doc/guides/password_requirements.rdoc
339
+ - doc/guides/paths.rdoc
340
+ - doc/guides/query_params.rdoc
341
+ - doc/guides/redirects.rdoc
342
+ - doc/guides/registration_field.rdoc
343
+ - doc/guides/require_mfa.rdoc
344
+ - doc/guides/reset_password_autologin.rdoc
345
+ - doc/guides/status_column.rdoc
346
+ - doc/guides/totp_or_recovery.rdoc
326
347
  - doc/http_basic_auth.rdoc
327
- - doc/internals.rdoc
328
348
  - doc/jwt.rdoc
329
349
  - doc/jwt_cors.rdoc
330
350
  - doc/jwt_refresh.rdoc
@@ -363,6 +383,7 @@ files:
363
383
  - doc/release_notes/1.9.0.txt
364
384
  - doc/release_notes/2.0.0.txt
365
385
  - doc/release_notes/2.1.0.txt
386
+ - doc/release_notes/2.2.0.txt
366
387
  - doc/remember.rdoc
367
388
  - doc/reset_password.rdoc
368
389
  - doc/session_expiration.rdoc