rodauth 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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