action_auth 1.7.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15d12d1b57a930d83e079185b0218a3715976bbbfef2e0d711523b2aae9bae85
4
- data.tar.gz: 899692e5c6110136cde4a48b919d73287e3c93c04dfc8986ea89384ef05a4f74
3
+ metadata.gz: c1644cd4614dbea75ccc32618beb37d9c6d0bff8126c4eb0166e09528dcd2464
4
+ data.tar.gz: bb7abe7774ba7690bacd9496dc3facc89d7eee8303289619323b25b7d1d64820
5
5
  SHA512:
6
- metadata.gz: 2acc300b848aa222fc929ba6622db8b539067a7ed128faba00684c57c470fd67d66b893d3fb16b64e2300bad79c3d2935ddb3f9daf4673d5b0f8aa241cb2b289
7
- data.tar.gz: cf88f1aacdf1fad6087e5669a1130754c856f6cbbc8638dfc17a59f0369bc5a2a62cb33b7d93a7fc04a3cc62e1f02d6747c4a316b88e8d40a88f1ef9bbebf68b
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.1.0, it employs the most modern authentication
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,20 +11,25 @@ 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. [Usage](#usage)
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
- 5. [Have I Been Pwned](#have-i-been-pwned)
19
- 6. [Magic Links](#magic-links)
20
- 7. [SMS Authentication](#sms-authentication)
21
- 8. [Account Deletion](#account-deletion)
22
- 9. [WebAuthn](#webauthn)
23
- 10. [Within Your Application](#within-your-application)
24
- 11. Customizing
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
- 12. [License](#license)
27
- 13. [Credits](#credits)
31
+ 13. [License](#license)
32
+ 14. [Credits](#credits)
28
33
 
29
34
 
30
35
  ## Minimum Requirements
@@ -93,6 +98,8 @@ ActionAuth.configure do |config|
93
98
  config.magic_link_enabled = true
94
99
  config.passkey_only = true # Allows sign in with only a passkey
95
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
96
103
  config.sms_auth_enabled = false
97
104
  config.verify_email_on_sign_in = true
98
105
  config.webauthn_enabled = true # defined?(WebAuthn)
@@ -139,12 +146,63 @@ These are the planned features for ActionAuth. The ones that are checked off are
139
146
 
140
147
  ✅ - Account Deletion
141
148
 
149
+ ✅ - Password Complexity Validation
150
+
151
+ ✅ - Rate Limiting
152
+
153
+ ✅ - Session Timeout
154
+
155
+ ✅ - HTTPS-only cookies in production
156
+
142
157
  ⏳ - Account Lockout
143
158
 
144
159
  ⏳ - Account Suspension
145
160
 
146
161
  ⏳ - Account Impersonation
147
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
+
148
206
  ## Usage
149
207
 
150
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 :new, status: :unprocessable_entity
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
- cookies.signed.permanent[:session_token] = { value: session_record.id, httponly: true }
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?
@@ -26,6 +26,8 @@ module ActionAuth
26
26
  return if check_if_email_is_verified(user)
27
27
  @session = user.sessions.create
28
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?
29
31
  session_token_hash[:domain] = :all if ActionAuth.configuration.insert_cookie_domain
30
32
  cookies.signed.permanent[:session_token] = session_token_hash
31
33
  redirect_to main_app.root_path, notice: "Signed in successfully"
@@ -38,7 +40,10 @@ module ActionAuth
38
40
  def destroy
39
41
  session = Current.user.sessions.find(params[:id])
40
42
  session.destroy
41
- cookies.delete(:session_token)
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)
42
47
  response.headers["Clear-Site-Data"] = '"cache","storage"'
43
48
  redirect_to main_app.root_path, notice: "That session has been logged out"
44
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
- cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
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 = "from@example.com"
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
@@ -1,3 +1,3 @@
1
1
  module ActionAuth
2
- VERSION = "1.7.3"
2
+ VERSION = "1.8.0"
3
3
  end
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.7.3
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-01-21 00:00:00.000000000 Z
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.2
134
+ rubygems_version: 3.6.6
135
135
  specification_version: 4
136
136
  summary: A simple Rails engine for authorization.
137
137
  test_files: []