rsb-auth 0.9.1 → 0.9.3

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: c3eb90b542687f335662080f73af84d46e55348664b2e99193452555715b5772
4
- data.tar.gz: 5f980b6c9c0042304eb15c8f7f114461bece133f02bea55ba088006e4c915b34
3
+ metadata.gz: 796fc07fdbd135fee58327f302a10989bba92cdbd9d060df01a9b479361b834d
4
+ data.tar.gz: 8b3f4d530bcedc6b96a9e139f2353474cc1f803cc5833d9a568554c4097517a3
5
5
  SHA512:
6
- metadata.gz: f333c9fa678dba4bb82b544178ef99654ffbacf1108b2b2bcb7b89b48196357f2a407710a35ab975113a52521f3d00c8680e0f068ed61f76c5f8a5b1fdae9f0b
7
- data.tar.gz: 8b26d96dd17ef6892d025faa04704ce502e01e632be6ae37e06c40cbe756d72c3bfaebdc634ddd9ec026aeca79a89a680b427e16bf6ae92f9cab98648d633358
6
+ metadata.gz: f329589372f35a266a7ba67fde98eed5b1f09d7b5113fc0a74ac81a22b6f5284f39791ef3b5cd3cf759a14f5880b5ae503e30f7d6849b7c503420c51bcdd93e7
7
+ data.tar.gz: 91565f320f7a1f32db2bab78a451b83b40f0b175325647b2157d31336a21b1c970ccc6d7cfa3150ff9cbaeed5d77d5baa1c1acbe467b0b370f2b6b0cdda9353f
@@ -51,7 +51,8 @@ module RSB
51
51
  cookies.signed[:rsb_session_token] = {
52
52
  value: session_record.token,
53
53
  httponly: true,
54
- same_site: :lax
54
+ same_site: :lax,
55
+ secure: Rails.env.production?
55
56
  }
56
57
  if result.identity.complete?
57
58
  redirect_to main_app.root_path, notice: 'Signed in.'
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bcrypt'
4
+
3
5
  module RSB
4
6
  module Auth
5
7
  class AuthenticationService
6
8
  Result = Data.define(:success?, :identity, :credential, :error, :unverified)
7
9
 
10
+ # Cached bcrypt digest for timing attack prevention.
11
+ DUMMY_DIGEST = BCrypt::Password.create('dummy_password_for_timing')
12
+
8
13
  # Authenticates by identifier and password. Only active (non-revoked) credentials
9
14
  # are considered. Also checks that the credential's type is currently enabled
10
15
  # via settings.
@@ -13,24 +18,29 @@ module RSB
13
18
  # @param password [String] password
14
19
  # @return [Result] success? with identity/credential, or error message
15
20
  def call(identifier:, password:)
16
- credential = RSB::Auth::Credential.active.find_by(
17
- identifier: identifier.strip.downcase
18
- )
21
+ normalized = identifier.strip.downcase
22
+ credential = RSB::Auth::Credential.active.find_by(identifier: normalized)
23
+
24
+ unless credential
25
+ # Perform a dummy bcrypt comparison to equalize response timing
26
+ DUMMY_DIGEST.is_password?(password)
27
+ return Result.new(success?: false, identity: nil, credential: nil, error: 'Invalid credentials.',
28
+ unverified: false)
29
+ end
19
30
 
20
- return failure('Invalid credentials.') unless credential
21
31
  return failure('Invalid credentials.') if credential.identity.deleted?
22
32
 
23
33
  # Check credential type is enabled
24
34
  credential_type_key = derive_credential_type_key(credential)
25
35
  unless RSB::Auth.credentials.enabled?(credential_type_key)
26
- return failure('This sign-in method is not available.')
36
+ return failure(error_message('This sign-in method is not available.'))
27
37
  end
28
38
 
29
- return failure('Account is locked. Try again later.') if credential.locked?
30
- return failure('Account is suspended.') if credential.identity.suspended?
39
+ return failure(error_message('Account is locked. Try again later.')) if credential.locked?
40
+ return failure(error_message('Account is suspended.')) if credential.identity.suspended?
31
41
 
32
42
  if credential.authenticate(password)
33
- credential.update_columns(failed_attempts: 0)
43
+ credential.update_columns(failed_attempts: 0) if credential.failed_attempts.positive?
34
44
 
35
45
  # Per-credential verification check
36
46
  verif_required = ActiveModel::Type::Boolean.new.cast(
@@ -60,6 +70,14 @@ module RSB
60
70
 
61
71
  private
62
72
 
73
+ def error_message(specific_message)
74
+ if RSB::Settings.get('auth.generic_error_messages')
75
+ 'Invalid credentials.'
76
+ else
77
+ specific_message
78
+ end
79
+ end
80
+
63
81
  # Derives the credential type key from a credential's STI type.
64
82
  # E.g., "RSB::Auth::Credential::EmailPassword" -> :email_password
65
83
  #
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRSBAuthTables < ActiveRecord::Migration[8.1]
4
+ def change
5
+ create_table :rsb_auth_identities do |t|
6
+ t.string :status, null: false, default: 'active'
7
+ t.json :metadata, default: {}
8
+ t.datetime :deleted_at
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :rsb_auth_identities, :deleted_at,
14
+ where: 'deleted_at IS NOT NULL',
15
+ name: 'index_rsb_auth_identities_on_deleted_at'
16
+
17
+ create_table :rsb_auth_credentials do |t|
18
+ t.references :identity, null: false, foreign_key: { to_table: :rsb_auth_identities }
19
+ t.string :type, null: false
20
+ t.string :identifier, null: false
21
+ t.string :password_digest, null: false
22
+ t.json :metadata, default: {}
23
+ t.datetime :verified_at
24
+ t.string :verification_token
25
+ t.datetime :verification_sent_at
26
+ t.integer :failed_attempts, null: false, default: 0
27
+ t.datetime :locked_until
28
+ t.datetime :revoked_at
29
+ t.string :recovery_email
30
+
31
+ t.timestamps
32
+ end
33
+
34
+ add_index :rsb_auth_credentials, %i[type identifier],
35
+ unique: true,
36
+ where: 'revoked_at IS NULL',
37
+ name: 'index_rsb_auth_credentials_on_type_and_identifier_active'
38
+ add_index :rsb_auth_credentials, :verification_token, unique: true
39
+ add_index :rsb_auth_credentials, :recovery_email
40
+
41
+ create_table :rsb_auth_sessions do |t|
42
+ t.references :identity, null: false, foreign_key: { to_table: :rsb_auth_identities }
43
+ t.string :token, null: false
44
+ t.string :ip_address
45
+ t.string :user_agent
46
+ t.datetime :last_active_at
47
+ t.datetime :expires_at, null: false
48
+
49
+ t.timestamps
50
+ end
51
+
52
+ add_index :rsb_auth_sessions, :token, unique: true
53
+ add_index :rsb_auth_sessions, :expires_at
54
+
55
+ create_table :rsb_auth_password_reset_tokens do |t|
56
+ t.references :credential, null: false, foreign_key: { to_table: :rsb_auth_credentials }
57
+ t.string :token, null: false
58
+ t.datetime :expires_at, null: false
59
+ t.datetime :used_at
60
+
61
+ t.timestamps
62
+ end
63
+
64
+ add_index :rsb_auth_password_reset_tokens, :token, unique: true
65
+
66
+ create_table :rsb_auth_invitations do |t|
67
+ t.string :email, null: false
68
+ t.string :token, null: false
69
+ t.references :invited_by, polymorphic: true, null: true
70
+ t.datetime :accepted_at
71
+ t.datetime :expires_at, null: false
72
+ t.datetime :revoked_at
73
+
74
+ t.timestamps
75
+ end
76
+
77
+ add_index :rsb_auth_invitations, :token, unique: true
78
+ add_index :rsb_auth_invitations, :email
79
+ end
80
+ end
@@ -28,7 +28,7 @@ module RSB
28
28
  class_name: 'RSB::Auth::Credential::EmailPassword',
29
29
  authenticatable: true,
30
30
  registerable: true,
31
- label: 'Email & Password',
31
+ label: 'Email',
32
32
  icon: 'mail',
33
33
  form_partial: 'rsb/auth/credentials/email_password',
34
34
  admin_form_partial: 'rsb/auth/admin/credentials/email_password'
@@ -41,7 +41,7 @@ module RSB
41
41
  class_name: 'RSB::Auth::Credential::UsernamePassword',
42
42
  authenticatable: true,
43
43
  registerable: true,
44
- label: 'Username & Password',
44
+ label: 'Username',
45
45
  icon: 'user',
46
46
  form_partial: 'rsb/auth/credentials/username_password',
47
47
  admin_form_partial: 'rsb/auth/admin/credentials/username_password'
@@ -67,6 +67,12 @@ module RSB
67
67
  group: 'Features',
68
68
  depends_on: 'auth.account_enabled',
69
69
  description: 'Enable self-service account deletion'
70
+
71
+ setting :generic_error_messages,
72
+ type: :boolean,
73
+ default: false,
74
+ group: 'Security',
75
+ description: 'When enabled, all login failures return "Invalid credentials" regardless of the actual reason (locked, suspended, disabled). Prevents account enumeration via error message differentiation.'
70
76
  end
71
77
  end
72
78
  end
@@ -4,6 +4,11 @@ module RSB
4
4
  module Auth
5
5
  module TestHelper
6
6
  def self.included(base)
7
+ base.setup do
8
+ RSB::Auth.reset!
9
+ Rails.cache.clear
10
+ end
11
+
7
12
  base.teardown do
8
13
  RSB::Auth.reset!
9
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rsb-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksandr Marchenko
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 0.9.1
46
+ version: 0.9.3
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 0.9.1
53
+ version: 0.9.3
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: useragent
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -142,15 +142,7 @@ files:
142
142
  - config/locales/credentials.en.yml
143
143
  - config/locales/seo.en.yml
144
144
  - config/routes.rb
145
- - db/migrate/20260208100001_create_rsb_auth_identities.rb
146
- - db/migrate/20260208100002_create_rsb_auth_credentials.rb
147
- - db/migrate/20260208100003_create_rsb_auth_sessions.rb
148
- - db/migrate/20260208100004_create_rsb_auth_password_reset_tokens.rb
149
- - db/migrate/20260208100005_add_verification_to_rsb_auth_credentials.rb
150
- - db/migrate/20260208100006_create_rsb_auth_invitations.rb
151
- - db/migrate/20260211100001_add_revoked_at_to_rsb_auth_credentials.rb
152
- - db/migrate/20260212100001_add_deleted_at_to_rsb_auth_identities.rb
153
- - db/migrate/20260214172956_add_recovery_email_to_rsb_auth_credentials.rb
145
+ - db/migrate/20260216100001_create_rsb_auth_tables.rb
154
146
  - lib/generators/rsb/auth/install/install_generator.rb
155
147
  - lib/generators/rsb/auth/views/views_generator.rb
156
148
  - lib/rsb/auth.rb
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRSBAuthIdentities < ActiveRecord::Migration[8.1]
4
- def change
5
- create_table :rsb_auth_identities do |t|
6
- t.string :status, null: false, default: 'active'
7
- t.json :metadata, default: {}
8
-
9
- t.timestamps
10
- end
11
- end
12
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRSBAuthCredentials < ActiveRecord::Migration[8.1]
4
- def change
5
- create_table :rsb_auth_credentials do |t|
6
- t.references :identity, null: false, foreign_key: { to_table: :rsb_auth_identities }
7
- t.string :type, null: false
8
- t.string :identifier, null: false
9
- t.string :password_digest, null: false
10
- t.json :metadata, default: {}
11
- t.datetime :verified_at
12
- t.integer :failed_attempts, null: false, default: 0
13
- t.datetime :locked_until
14
-
15
- t.timestamps
16
- end
17
-
18
- add_index :rsb_auth_credentials, %i[type identifier], unique: true
19
- end
20
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRSBAuthSessions < ActiveRecord::Migration[8.1]
4
- def change
5
- create_table :rsb_auth_sessions do |t|
6
- t.references :identity, null: false, foreign_key: { to_table: :rsb_auth_identities }
7
- t.string :token, null: false
8
- t.string :ip_address
9
- t.string :user_agent
10
- t.datetime :last_active_at
11
- t.datetime :expires_at, null: false
12
- t.timestamps
13
- end
14
-
15
- add_index :rsb_auth_sessions, :token, unique: true
16
- add_index :rsb_auth_sessions, :expires_at
17
- end
18
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRSBAuthPasswordResetTokens < ActiveRecord::Migration[8.1]
4
- def change
5
- create_table :rsb_auth_password_reset_tokens do |t|
6
- t.references :credential, null: false, foreign_key: { to_table: :rsb_auth_credentials }
7
- t.string :token, null: false
8
- t.datetime :expires_at, null: false
9
- t.datetime :used_at
10
- t.timestamps
11
- end
12
-
13
- add_index :rsb_auth_password_reset_tokens, :token, unique: true
14
- end
15
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddVerificationToRSBAuthCredentials < ActiveRecord::Migration[8.1]
4
- def change
5
- add_column :rsb_auth_credentials, :verification_token, :string
6
- add_column :rsb_auth_credentials, :verification_sent_at, :datetime
7
- add_index :rsb_auth_credentials, :verification_token, unique: true
8
- end
9
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateRSBAuthInvitations < ActiveRecord::Migration[8.1]
4
- def change
5
- create_table :rsb_auth_invitations do |t|
6
- t.string :email, null: false
7
- t.string :token, null: false
8
- t.references :invited_by, polymorphic: true, null: true
9
- t.datetime :accepted_at
10
- t.datetime :expires_at, null: false
11
- t.datetime :revoked_at
12
-
13
- t.timestamps
14
- end
15
-
16
- add_index :rsb_auth_invitations, :token, unique: true
17
- add_index :rsb_auth_invitations, :email
18
- end
19
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddRevokedAtToRSBAuthCredentials < ActiveRecord::Migration[8.1]
4
- def change
5
- add_column :rsb_auth_credentials, :revoked_at, :datetime, null: true
6
-
7
- # Replace full unique index with partial unique index scoped to active credentials.
8
- # This allows the same [type, identifier] to exist multiple times as long as
9
- # only one is active (revoked_at IS NULL). Supported by PostgreSQL and SQLite 3.8.0+.
10
- remove_index :rsb_auth_credentials, %i[type identifier]
11
- add_index :rsb_auth_credentials, %i[type identifier],
12
- unique: true,
13
- where: 'revoked_at IS NULL',
14
- name: 'index_rsb_auth_credentials_on_type_and_identifier_active'
15
- end
16
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddDeletedAtToRSBAuthIdentities < ActiveRecord::Migration[8.1]
4
- def change
5
- add_column :rsb_auth_identities, :deleted_at, :datetime, null: true
6
- add_index :rsb_auth_identities, :deleted_at,
7
- where: 'deleted_at IS NOT NULL',
8
- name: 'index_rsb_auth_identities_on_deleted_at'
9
- end
10
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddRecoveryEmailToRSBAuthCredentials < ActiveRecord::Migration[8.0]
4
- def change
5
- add_column :rsb_auth_credentials, :recovery_email, :string
6
- add_index :rsb_auth_credentials, :recovery_email
7
- end
8
- end