action_auth 1.7.2 → 1.8.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.
- checksums.yaml +4 -4
- data/README.md +72 -45
- data/app/controllers/action_auth/passwords_controller.rb +14 -1
- data/app/controllers/action_auth/registrations_controller.rb +16 -1
- data/app/controllers/action_auth/sessions_controller.rb +12 -1
- data/app/controllers/action_auth/webauthn_credential_authentications_controller.rb +10 -1
- data/app/models/action_auth/user.rb +11 -0
- data/lib/action_auth/configuration.rb +10 -2
- data/lib/action_auth/controllers/helpers.rb +28 -0
- data/lib/action_auth/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1644cd4614dbea75ccc32618beb37d9c6d0bff8126c4eb0166e09528dcd2464
|
4
|
+
data.tar.gz: bb7abe7774ba7690bacd9496dc3facc89d7eee8303289619323b25b7d1d64820
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b33191d590c1f42d70e3181ff62d38bf2bf65475e35fd9cccc360c6439b0ea5f30328891ed2f4e18ee30cc09cb6ec14181edadf33d67b8d4cc9e8a18ff066660
|
7
|
+
data.tar.gz: d38436d8e8361c6d5f7f3d22e958e5b8798d448068a43875691c0cef08c6fdc34ed6a431dcecf2bf2d71f5269d8c2394461b06f91815d7d6a4aed070086254ed
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ActionAuth
|
2
2
|
ActionAuth is an authentication Rails engine crafted to integrate seamlessly
|
3
|
-
with your Rails application. Optimized for Rails 7.
|
3
|
+
with your Rails application. Optimized for Rails 7.2.0, it employs the most modern authentication
|
4
4
|
techniques and streamlined token reset processes. Its simplicity and ease of use let you concentrate
|
5
5
|
on developing your application, while its reliance on ActiveSupport::CurrentAttributes ensures a
|
6
6
|
user experience akin to that offered by the well-regarded Devise gem.
|
@@ -11,57 +11,31 @@ user experience akin to that offered by the well-regarded Devise gem.
|
|
11
11
|
1. [Introduction](#introduction)
|
12
12
|
2. [Installation](#installation)
|
13
13
|
3. [Features](#features)
|
14
|
-
4. [
|
14
|
+
4. [Security Features](#security-features)
|
15
|
+
- [Password Security](#password-security)
|
16
|
+
- [Session Security](#session-security)
|
17
|
+
- [Rate Limiting](#rate-limiting)
|
18
|
+
- [Multi-Factor Authentication](#multi-factor-authentication)
|
19
|
+
5. [Usage](#usage)
|
15
20
|
- [Routes](#routes)
|
16
21
|
- [Helper Methods](#helper-methods)
|
17
22
|
- [Restricting and Changing Routes](#restricting-and-changing-routes)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
6. [Have I Been Pwned](#have-i-been-pwned)
|
24
|
+
7. [Magic Links](#magic-links)
|
25
|
+
8. [SMS Authentication](#sms-authentication)
|
26
|
+
9. [Account Deletion](#account-deletion)
|
27
|
+
10. [WebAuthn](#webauthn)
|
28
|
+
11. [Within Your Application](#within-your-application)
|
29
|
+
12. Customizing
|
25
30
|
- [Sign In Page](https://github.com/kobaltz/action_auth/wiki/Overriding-Sign-In-page-view)
|
26
|
-
|
27
|
-
|
31
|
+
13. [License](#license)
|
32
|
+
14. [Credits](#credits)
|
28
33
|
|
29
|
-
## Breaking Changes
|
30
34
|
|
31
|
-
|
32
|
-
biggest change is that the `ActionAuth::User` model now uses the table name of `users` instead
|
33
|
-
of `action_auth_users`. This was done to make it easier to integrate with your application
|
34
|
-
without having to worry about the table name. If you have an existing application that is
|
35
|
-
using ActionAuth, you will need to rename the table to `users` with a migration like
|
35
|
+
## Minimum Requirements
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
```
|
40
|
-
|
41
|
-
Coming from `v0.3.0` to `v1.0.0`, you will need to create a migration to rename the table and foreign keys.
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
class UpgradeActionAuth < ActiveRecord::Migration[7.1]
|
45
|
-
def change
|
46
|
-
rename_table :action_auth_users, :users
|
47
|
-
|
48
|
-
rename_table :action_auth_sessions, :sessions
|
49
|
-
rename_column :sessions, :action_auth_user_id, :user_id
|
50
|
-
|
51
|
-
rename_table :action_auth_webauthn_credentials, :webauthn_credentials
|
52
|
-
rename_column :webauthn_credentials, :action_auth_user_id, :user_id
|
53
|
-
end
|
54
|
-
end
|
55
|
-
```
|
56
|
-
|
57
|
-
You will then need to undo the migrations where the foreign keys were added in cases where `foreign_key: true` was
|
58
|
-
changed to `foreign_key: { to_table: 'action_auth_users' }`. You can do this for each table with a migration like:
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
add_foreign_key :user_settings, :users, column: :user_id unless foreign_key_exists?(:user_settings, :users)
|
62
|
-
add_foreign_key :profiles, :users, column: :user_id unless foreign_key_exists?(:profiles, :users)
|
63
|
-
add_foreign_key :nfcs, :users, column: :user_id unless foreign_key_exists?(:nfcs, :users)
|
64
|
-
```
|
37
|
+
- Ruby 3.3.0 or later recommended
|
38
|
+
- Rails 7.2.0 or later **required**
|
65
39
|
|
66
40
|
## Installation
|
67
41
|
|
@@ -124,6 +98,8 @@ ActionAuth.configure do |config|
|
|
124
98
|
config.magic_link_enabled = true
|
125
99
|
config.passkey_only = true # Allows sign in with only a passkey
|
126
100
|
config.pwned_enabled = true # defined?(Pwned)
|
101
|
+
config.password_complexity_check = true # Requires complex passwords
|
102
|
+
config.session_timeout = 2.weeks # Session expires after this period of inactivity
|
127
103
|
config.sms_auth_enabled = false
|
128
104
|
config.verify_email_on_sign_in = true
|
129
105
|
config.webauthn_enabled = true # defined?(WebAuthn)
|
@@ -170,12 +146,63 @@ These are the planned features for ActionAuth. The ones that are checked off are
|
|
170
146
|
|
171
147
|
✅ - Account Deletion
|
172
148
|
|
149
|
+
✅ - Password Complexity Validation
|
150
|
+
|
151
|
+
✅ - Rate Limiting
|
152
|
+
|
153
|
+
✅ - Session Timeout
|
154
|
+
|
155
|
+
✅ - HTTPS-only cookies in production
|
156
|
+
|
173
157
|
⏳ - Account Lockout
|
174
158
|
|
175
159
|
⏳ - Account Suspension
|
176
160
|
|
177
161
|
⏳ - Account Impersonation
|
178
162
|
|
163
|
+
## Security Features
|
164
|
+
|
165
|
+
ActionAuth comes with a robust set of security features designed to protect user accounts and data:
|
166
|
+
|
167
|
+
### Password Security
|
168
|
+
- Minimum password length of 12 characters
|
169
|
+
- Password complexity validation requiring uppercase, lowercase, numbers, and special characters
|
170
|
+
- Integration with Have I Been Pwned to check for compromised passwords
|
171
|
+
- Password complexity validation can be configured to suit your application's needs
|
172
|
+
|
173
|
+
### Session Security
|
174
|
+
- Session timeout with configurable duration (default: 2 weeks)
|
175
|
+
- Automatic session invalidation on password change
|
176
|
+
- HTTPS-only cookies in production environments
|
177
|
+
- HttpOnly flag on cookies to prevent JavaScript access
|
178
|
+
- SameSite=Lax attribute to prevent CSRF attacks
|
179
|
+
- IP address and user agent tracking to detect session hijacking
|
180
|
+
- Suspicious activity detection for changed IP/user agent
|
181
|
+
|
182
|
+
### Rate Limiting
|
183
|
+
- Protection against brute force attacks on login
|
184
|
+
- Rate limiting on registration attempts
|
185
|
+
- Rate limiting on password reset attempts
|
186
|
+
- Rate limiting on WebAuthn authentication
|
187
|
+
|
188
|
+
### Multi-Factor Authentication
|
189
|
+
- Support for WebAuthn/passkeys as a second factor
|
190
|
+
- Modern security key and biometric authentication support
|
191
|
+
- Magic link authentication as an alternative authentication method
|
192
|
+
|
193
|
+
### Configuration Options
|
194
|
+
```ruby
|
195
|
+
ActionAuth.configure do |config|
|
196
|
+
# Enable password complexity validation
|
197
|
+
config.password_complexity_check = true
|
198
|
+
|
199
|
+
# Set session timeout (defaults to 2 weeks)
|
200
|
+
config.session_timeout = 2.weeks
|
201
|
+
|
202
|
+
# Other settings as needed...
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
179
206
|
## Usage
|
180
207
|
|
181
208
|
### Routes
|
@@ -3,6 +3,12 @@ module ActionAuth
|
|
3
3
|
before_action :set_user
|
4
4
|
before_action :validate_pwned_password, only: :update
|
5
5
|
|
6
|
+
rate_limit to: 3,
|
7
|
+
within: 60.seconds,
|
8
|
+
only: :update,
|
9
|
+
name: "password-reset-throttle",
|
10
|
+
with: -> { redirect_to sign_in_path, alert: "Too many password reset attempts. Try again later." }
|
11
|
+
|
6
12
|
def edit
|
7
13
|
end
|
8
14
|
|
@@ -27,10 +33,17 @@ module ActionAuth
|
|
27
33
|
def validate_pwned_password
|
28
34
|
return unless ActionAuth.configuration.pwned_enabled?
|
29
35
|
|
36
|
+
# Check minimum password requirements
|
37
|
+
if params[:password].present? && ActionAuth.configuration.password_complexity_check? && !Rails.env.test? &&
|
38
|
+
(params[:password] !~ /[A-Z]/ || params[:password] !~ /[a-z]/ || params[:password] !~ /[0-9]/ || params[:password] !~ /[^A-Za-z0-9]/)
|
39
|
+
@user.errors.add(:password, "must include at least one uppercase letter, one lowercase letter, one number, and one special character.")
|
40
|
+
render :edit, status: :unprocessable_entity and return
|
41
|
+
end
|
42
|
+
|
30
43
|
pwned = Pwned::Password.new(params[:password])
|
31
44
|
if pwned.pwned?
|
32
45
|
@user.errors.add(:password, "has been pwned #{pwned.pwned_count} times. Please choose a different password.")
|
33
|
-
render :
|
46
|
+
render :edit, status: :unprocessable_entity
|
34
47
|
end
|
35
48
|
end
|
36
49
|
end
|
@@ -2,6 +2,12 @@ module ActionAuth
|
|
2
2
|
class RegistrationsController < ApplicationController
|
3
3
|
before_action :validate_pwned_password, only: :create
|
4
4
|
|
5
|
+
rate_limit to: 3,
|
6
|
+
within: 60.seconds,
|
7
|
+
only: :create,
|
8
|
+
name: "registration-throttle",
|
9
|
+
with: -> { redirect_to new_user_registration_path, alert: "Too many registration attempts. Try again later." }
|
10
|
+
|
5
11
|
def new
|
6
12
|
@user = User.new
|
7
13
|
end
|
@@ -15,7 +21,10 @@ module ActionAuth
|
|
15
21
|
redirect_to sign_in_path, notice: "Welcome! You have signed up successfully. Please check your email to verify your account."
|
16
22
|
else
|
17
23
|
session_record = @user.sessions.create!
|
18
|
-
|
24
|
+
cookie_options = { value: session_record.id, httponly: true }
|
25
|
+
cookie_options[:secure] = Rails.env.production? if Rails.env.production?
|
26
|
+
cookie_options[:same_site] = :lax unless Rails.env.test?
|
27
|
+
cookies.signed.permanent[:session_token] = cookie_options
|
19
28
|
|
20
29
|
redirect_to sign_in_path, notice: "Welcome! You have signed up successfully"
|
21
30
|
end
|
@@ -37,6 +46,12 @@ module ActionAuth
|
|
37
46
|
def validate_pwned_password
|
38
47
|
return unless ActionAuth.configuration.pwned_enabled?
|
39
48
|
|
49
|
+
if params[:password].present? && !Rails.env.test? && (params[:password] !~ /[A-Z]/ || params[:password] !~ /[a-z]/ || params[:password] !~ /[0-9]/ || params[:password] !~ /[^A-Za-z0-9]/)
|
50
|
+
@user = User.new(email: params[:email])
|
51
|
+
@user.errors.add(:password, "must include at least one uppercase letter, one lowercase letter, one number, and one special character.")
|
52
|
+
render :new, status: :unprocessable_entity and return
|
53
|
+
end
|
54
|
+
|
40
55
|
pwned = Pwned::Password.new(params[:password])
|
41
56
|
|
42
57
|
if pwned.pwned?
|
@@ -3,6 +3,12 @@ module ActionAuth
|
|
3
3
|
before_action :set_current_request_details
|
4
4
|
before_action :authenticate_user!, only: [:index, :destroy]
|
5
5
|
|
6
|
+
rate_limit to: 5,
|
7
|
+
within: 20.seconds,
|
8
|
+
only: :create,
|
9
|
+
name: "slow-throttle",
|
10
|
+
with: -> { redirect_to sign_in_path, alert: "Try again later." }
|
11
|
+
|
6
12
|
def index
|
7
13
|
@action_auth_wide = true
|
8
14
|
@sessions = Current.user.sessions.order(created_at: :desc)
|
@@ -20,6 +26,8 @@ module ActionAuth
|
|
20
26
|
return if check_if_email_is_verified(user)
|
21
27
|
@session = user.sessions.create
|
22
28
|
session_token_hash = { value: @session.id, httponly: true }
|
29
|
+
session_token_hash[:secure] = Rails.env.production? if Rails.env.production?
|
30
|
+
session_token_hash[:same_site] = :lax unless Rails.env.test?
|
23
31
|
session_token_hash[:domain] = :all if ActionAuth.configuration.insert_cookie_domain
|
24
32
|
cookies.signed.permanent[:session_token] = session_token_hash
|
25
33
|
redirect_to main_app.root_path, notice: "Signed in successfully"
|
@@ -32,7 +40,10 @@ module ActionAuth
|
|
32
40
|
def destroy
|
33
41
|
session = Current.user.sessions.find(params[:id])
|
34
42
|
session.destroy
|
35
|
-
|
43
|
+
cookie_options = {}
|
44
|
+
cookie_options[:secure] = Rails.env.production? if Rails.env.production?
|
45
|
+
cookie_options[:same_site] = :lax unless Rails.env.test?
|
46
|
+
cookies.delete(:session_token, cookie_options)
|
36
47
|
response.headers["Clear-Site-Data"] = '"cache","storage"'
|
37
48
|
redirect_to main_app.root_path, notice: "That session has been logged out"
|
38
49
|
end
|
@@ -3,6 +3,12 @@ class ActionAuth::WebauthnCredentialAuthenticationsController < ApplicationContr
|
|
3
3
|
before_action :ensure_login_initiated
|
4
4
|
layout "action_auth/application"
|
5
5
|
|
6
|
+
rate_limit to: 5,
|
7
|
+
within: 20.seconds,
|
8
|
+
only: :create,
|
9
|
+
name: "webauthn-throttle",
|
10
|
+
with: -> { redirect_to sign_in_path, alert: "Too many attempts. Try again later." }
|
11
|
+
|
6
12
|
def new
|
7
13
|
get_options = WebAuthn::Credential.options_for_get(allow: user.webauthn_credentials.pluck(:external_id))
|
8
14
|
session[:current_challenge] = get_options.challenge
|
@@ -24,7 +30,10 @@ class ActionAuth::WebauthnCredentialAuthenticationsController < ApplicationContr
|
|
24
30
|
credential.update!(sign_count: webauthn_credential.sign_count)
|
25
31
|
session.delete(:webauthn_user_id)
|
26
32
|
session = user.sessions.create
|
27
|
-
|
33
|
+
cookie_options = { value: session.id, httponly: true }
|
34
|
+
cookie_options[:secure] = Rails.env.production? if Rails.env.production?
|
35
|
+
cookie_options[:same_site] = :lax unless Rails.env.test?
|
36
|
+
cookies.signed.permanent[:session_token] = cookie_options
|
28
37
|
render json: { status: "ok" }, status: :ok
|
29
38
|
rescue WebAuthn::Error => e
|
30
39
|
Rails.logger.error "❌ Verification failed: #{e.message}"
|
@@ -26,6 +26,7 @@ module ActionAuth
|
|
26
26
|
|
27
27
|
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
28
28
|
validates :password, allow_nil: true, length: { minimum: 12 }
|
29
|
+
validate :password_complexity, if: -> { ActionAuth.configuration.password_complexity_check? && password.present? && !Rails.env.test? }
|
29
30
|
validates :phone_number,
|
30
31
|
allow_nil: true,
|
31
32
|
uniqueness: true,
|
@@ -49,5 +50,15 @@ module ActionAuth
|
|
49
50
|
return false unless ActionAuth.configuration.webauthn_enabled?
|
50
51
|
webauthn_credentials.any?
|
51
52
|
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def password_complexity
|
57
|
+
return if password.blank?
|
58
|
+
|
59
|
+
unless password =~ /[A-Z]/ && password =~ /[a-z]/ && password =~ /[0-9]/ && password =~ /[^A-Za-z0-9]/
|
60
|
+
errors.add(:password, "must include at least one uppercase letter, one lowercase letter, one number, and one special character")
|
61
|
+
end
|
62
|
+
end
|
52
63
|
end
|
53
64
|
end
|
@@ -12,21 +12,25 @@ module ActionAuth
|
|
12
12
|
attr_accessor :webauthn_enabled
|
13
13
|
attr_accessor :webauthn_origin
|
14
14
|
attr_accessor :webauthn_rp_name
|
15
|
+
attr_accessor :password_complexity_check
|
16
|
+
attr_accessor :session_timeout
|
15
17
|
|
16
18
|
attr_accessor :insert_cookie_domain
|
17
19
|
|
18
20
|
def initialize
|
19
21
|
@allow_user_deletion = true
|
20
|
-
@default_from_email = "
|
22
|
+
@default_from_email = Rails.application.config.action_mailer.default_options&.dig(:from) || "noreply@#{ENV['HOST'] || 'example.com'}"
|
21
23
|
@magic_link_enabled = true
|
22
24
|
@passkey_only = true
|
23
25
|
@pwned_enabled = defined?(Pwned)
|
26
|
+
@password_complexity_check = true
|
24
27
|
@sms_auth_enabled = false
|
25
28
|
@sms_send_class = nil
|
26
29
|
@verify_email_on_sign_in = true
|
27
30
|
@webauthn_enabled = defined?(WebAuthn)
|
28
|
-
@webauthn_origin = "http://localhost:3000"
|
31
|
+
@webauthn_origin = Rails.env.production? ? "https://#{ENV['HOST']}" : "http://localhost:3000"
|
29
32
|
@webauthn_rp_name = Rails.application.class.to_s.deconstantize
|
33
|
+
@session_timeout = 2.weeks
|
30
34
|
|
31
35
|
@insert_cookie_domain = false
|
32
36
|
end
|
@@ -55,5 +59,9 @@ module ActionAuth
|
|
55
59
|
@pwned_enabled.respond_to?(:call) ? @pwned_enabled.call : @pwned_enabled
|
56
60
|
end
|
57
61
|
|
62
|
+
def password_complexity_check?
|
63
|
+
@password_complexity_check == true
|
64
|
+
end
|
65
|
+
|
58
66
|
end
|
59
67
|
end
|
@@ -5,6 +5,7 @@ module ActionAuth
|
|
5
5
|
|
6
6
|
included do
|
7
7
|
before_action :set_current_request_details
|
8
|
+
before_action :check_session_timeout
|
8
9
|
|
9
10
|
def current_user; Current.user; end
|
10
11
|
helper_method :current_user
|
@@ -28,6 +29,33 @@ module ActionAuth
|
|
28
29
|
Current.session = Session.find_by(id: cookies.signed[:session_token])
|
29
30
|
Current.user_agent = request.user_agent
|
30
31
|
Current.ip_address = request.ip
|
32
|
+
|
33
|
+
# Check if IP address or user agent has changed
|
34
|
+
if Current.session && (Current.session.ip_address != Current.ip_address ||
|
35
|
+
Current.session.user_agent != Current.user_agent)
|
36
|
+
Rails.logger.warn "Session environment changed for user #{Current.user&.id}: IP or User-Agent mismatch"
|
37
|
+
# Optional: Force re-authentication for suspicious activity
|
38
|
+
# cookies.delete(:session_token, secure: Rails.env.production?, same_site: :lax)
|
39
|
+
# Current.session = nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_session_timeout
|
44
|
+
return unless Current.session
|
45
|
+
return if Rails.env.test?
|
46
|
+
|
47
|
+
timeout = ActionAuth.configuration.session_timeout
|
48
|
+
if Current.session.updated_at < timeout.ago
|
49
|
+
Rails.logger.info "Session timed out for user #{Current.user&.id}"
|
50
|
+
Current.session.destroy
|
51
|
+
cookie_options = {}
|
52
|
+
cookie_options[:secure] = Rails.env.production? if Rails.env.production?
|
53
|
+
cookie_options[:same_site] = :lax unless Rails.env.test?
|
54
|
+
cookies.delete(:session_token, cookie_options)
|
55
|
+
redirect_to new_user_session_path, alert: "Your session has expired. Please sign in again."
|
56
|
+
else
|
57
|
+
Current.session.touch
|
58
|
+
end
|
31
59
|
end
|
32
60
|
end
|
33
61
|
end
|
data/lib/action_auth/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dave Kimura
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-03-16 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -131,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
131
|
- !ruby/object:Gem::Version
|
132
132
|
version: '0'
|
133
133
|
requirements: []
|
134
|
-
rubygems_version: 3.6.
|
134
|
+
rubygems_version: 3.6.6
|
135
135
|
specification_version: 4
|
136
136
|
summary: A simple Rails engine for authorization.
|
137
137
|
test_files: []
|