add_auth 0.4.0 → 0.5.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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +34 -5
  4. data/ROADMAP.md +6 -0
  5. data/SECURITY.md +4 -0
  6. data/app/controllers/add_auth/sessions_controller.rb +1 -1
  7. data/lib/add_auth/core/session_cursor.rb +38 -0
  8. data/lib/add_auth/core/session_key.rb +32 -0
  9. data/lib/add_auth/core/sessions.rb +19 -8
  10. data/lib/add_auth/rails/migration/key_type.rb +3 -3
  11. data/lib/add_auth/rails/stores/sessions.rb +10 -2
  12. data/lib/add_auth/version.rb +1 -1
  13. data/lib/generators/add_auth/accounts/accounts_generator.rb +2 -0
  14. data/lib/generators/add_auth/accounts/templates/add_add_auth_accounts.rb.tt +2 -2
  15. data/lib/generators/add_auth/email_tokens/email_tokens_generator.rb +2 -0
  16. data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +1 -1
  17. data/lib/generators/add_auth/external_identities/external_identities_generator.rb +2 -0
  18. data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt +6 -12
  19. data/lib/generators/add_auth/mobile_sessions/mobile_sessions_generator.rb +2 -0
  20. data/lib/generators/add_auth/mobile_sessions/templates/create_add_auth_mobile_handoffs.rb.tt +4 -4
  21. data/lib/generators/add_auth/notifications/notifications_generator.rb +2 -0
  22. data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +1 -1
  23. data/lib/generators/add_auth/passkeys/passkeys_generator.rb +2 -0
  24. data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +2 -2
  25. data/lib/generators/add_auth/primary_key.rb +19 -0
  26. data/lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb +3 -0
  27. data/lib/generators/add_auth/session_upgrade/templates/add_add_auth_session_cursor_index.rb.tt +5 -0
  28. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ed4ef07ed6308f6bb4b82de3e67552af3604472f38e7a379f1d9c863f76954e
4
- data.tar.gz: 8189c0ddf517a9d06d291738e0d0d20bb383ffc9a7fdd085c9df33ad914312d4
3
+ metadata.gz: b83eabd84666412ae2a83ffc20ac54c9765ef0133e0e401cffebce9e3f73f444
4
+ data.tar.gz: b187c3b70d0c43139f870a50231e5e024f71885218ed68770c98218c257b4be3
5
5
  SHA512:
6
- metadata.gz: 643216bb646e0ca6032f0b53f1928eb6da0b4efeffccadd8477fbb7776f6aa411c6ecf36bb4c3f785d94fa435320c04745a2565c70cef47e387f14fc534b805b
7
- data.tar.gz: 48375790d81d186fa5cfd6f4f3ecf4672c46b2ec8602a3597c5a40b69562661212de41f5df8676a7080ef96ab36915af6dec798400aa97610b5b3c22d76be2d4
6
+ metadata.gz: 65eb201de7ebebca70a786c9664a46df8c27404e74dcd2704b25ef33673ab0f42f7a9c5a865c707cb33a943a4a933c8903fc5dafd1479c32bf0ad0266263f7d6
7
+ data.tar.gz: a48427dfe1145c18be90ddb4ec027b70a7179f7e742d3806ee99ee2ddbf7e9f865abbb0486d96cce600e7e5284ee7487ea7a4b17c71639b94c1801dc95b1b8ee
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-09-18
4
+
5
+ - Honor Rails' `primary_key_type` generator setting for all AddAuth-owned tables.
6
+ Support PostgreSQL UUID sessions and mixed integer/UUID account/session keys,
7
+ retaining bigint defaults and deriving references from the existing schema.
8
+ - Use creation time and a typed ID in opaque session-page cursors; validate
9
+ revocation and explicitly enabled legacy session adoption for either key type.
10
+ For apps already using hardened sessions, re-run `add_auth:session_upgrade`
11
+ for the new pagination index. Existing tables retain their IDs; refresh ejected
12
+ controllers through the normal review process.
13
+ Follow the [upgrade guide](https://addauthgem.com/upgrading/#uuid-support) for
14
+ the release gate, migration review, opaque client cursors and rollback boundary.
15
+
3
16
  ## 0.4.0 — 2026-09-13
4
17
 
5
18
  - Constrain JSON below 3.0 for the supported Rails decoding API; independently
data/README.md CHANGED
@@ -29,26 +29,26 @@ for verification in a disposable host.
29
29
 
30
30
  ## Quickstart
31
31
 
32
- These commands target 0.4.0 and are available after its
33
- [RubyGems publication](https://rubygems.org/gems/add_auth/versions/0.4.0).
32
+ These commands target 0.5.0 and are available after its
33
+ [RubyGems publication](https://rubygems.org/gems/add_auth/versions/0.5.0).
34
34
  Check package availability before changing your app’s bundle.
35
35
 
36
36
  Install the `add_auth` gem from RubyGems through your Rails app’s Gemfile,
37
37
  then enable password and email-link sign-in. Use Ruby 3.3+ and Rails 8.0+
38
38
  with Active Record, and run the commands below from your Rails app’s root.
39
39
 
40
- **Release availability:** these commands require the published `0.4.0` package.
40
+ **Release availability:** these commands require the published `0.5.0` package.
41
41
  If it is not yet listed on [RubyGems](https://rubygems.org/gems/add_auth/versions),
42
42
  wait for publication; see [release status](https://addauthgem.com/release-status/).
43
43
  Start in your app’s development environment; use the deployment settings
44
44
  below before enabling sign-in for users.
45
45
 
46
46
  1. Keep `source "https://rubygems.org"` in your app’s Gemfile and add the
47
- 0.4 release line:
47
+ 0.5 release line:
48
48
 
49
49
  ```ruby
50
50
  # Gemfile
51
- gem "add_auth", "~> 0.4.0"
51
+ gem "add_auth", "~> 0.5.0"
52
52
  ```
53
53
 
54
54
  Bundler downloads the package from RubyGems and records the resolved
@@ -568,6 +568,35 @@ adds registration, confirmation/reconfirmation, password reset, lock/unlock and
568
568
  account changes. Hosts retain account eligibility, resource authorization and
569
569
  provider registrations.
570
570
 
571
+ ### UUID and integer primary keys in 0.5.0
572
+
573
+ Version 0.5.0 adds PostgreSQL UUID sessions and configurable primary keys for all
574
+ AddAuth authentication tables. Integer apps retain the normal Rails defaults.
575
+ These instructions become available after the 0.5.0 package is published.
576
+
577
+ For a new PostgreSQL app, set Rails' generator preference in `config/application.rb`
578
+ before generating authentication or AddAuth features:
579
+
580
+ ```ruby
581
+ config.generators do |g|
582
+ g.orm :active_record, primary_key_type: :uuid
583
+ end
584
+ ```
585
+
586
+ AddAuth uses this setting for every table it creates. Leave it unset for Rails'
587
+ bigint default, or set `:bigint` or `:integer` explicitly. References follow the
588
+ actual target table, so UUID users can have bigint sessions and vice versa.
589
+ The generated migration records its chosen type; later configuration changes do
590
+ not change that migration or convert existing rows. Other key types are unsupported.
591
+
592
+ For an existing installation, follow the
593
+ [version-gated package upgrade](https://addauthgem.com/upgrading/#uuid-support).
594
+ It covers the additive pagination index, ejected controller review, opaque
595
+ pagination cursors, verification and rollback from older gems. Integer apps need
596
+ no ID conversion. The [primary-key guide](https://addauthgem.com/uuid-primary-keys/)
597
+ explains mixed schemas and the separate, app-owned migration needed to convert
598
+ existing IDs. The published 0.4.0 package cannot manage UUID sessions.
599
+
571
600
  ## Optional integrations in 0.3.0
572
601
 
573
602
  Enable bounded [Devise migration](https://addauthgem.com/migrating-from-devise/),
data/ROADMAP.md CHANGED
@@ -25,6 +25,12 @@ this public checklist is derived from its section 14 and stands on its own.
25
25
 
26
26
  ## Now — maintain AddAuth and learn from adoption
27
27
 
28
+ - [x] Honor the Rails UUID/bigint generator setting across all authentication
29
+ tables, including UUID sessions, mixed references and three-mode bundled/ejected
30
+ session management. Local PostgreSQL and browser acceptance passes on Rails
31
+ 8.0/8.1. Added in 0.5.0; see
32
+ [release availability](https://github.com/taimoorq/add_auth/releases).
33
+
28
34
  - [x] Optional password-signup confirmation, immediate transactional provisioning
29
35
  and session issuance, explicit unconfirmed-reset policy, password-only lifecycle
30
36
  and the complete commented Rails initializer. Local supported-matrix, stock/
data/SECURITY.md CHANGED
@@ -54,6 +54,10 @@ the currency policy).
54
54
 
55
55
  ## Supported versions
56
56
 
57
+ UUID/integer session lookup, account-scoped pagination and revocation are also
58
+ in scope. Version 0.5.0 adds UUID sessions and UUID authentication-table generation;
59
+ primary keys must never substitute for browser/mobile bearer secrets.
60
+
57
61
  Until a 1.0 is released, only the latest published version receives security
58
62
  fixes. This table will be expanded once there are stable release lines to
59
63
  support.
@@ -25,7 +25,7 @@ module AddAuth
25
25
 
26
26
  def destroy
27
27
  target_id = params[:id].to_s
28
- if target_id.match?(/\A[1-9]\d*\z/) && target_id.to_i == Current.session.id
28
+ if Rails::Runtime.sessions.current_session?(session: Current.session, session_id: target_id)
29
29
  terminate_session
30
30
  return redirect_to Rails::Runtime.sign_in_path, status: :see_other
31
31
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "time"
6
+
7
+ module AddAuth
8
+ module Core
9
+ class SessionCursor
10
+ def initialize(key:)
11
+ @key = key
12
+ end
13
+
14
+ def encode(row)
15
+ "sc1:" + Base64.urlsafe_encode64(JSON.generate([row.created_at.utc.iso8601(6), row.id]), padding: false)
16
+ end
17
+
18
+ def decode(value)
19
+ # Retain links issued by releases with integer-only pagination.
20
+ legacy = @key.parse(value)
21
+ return {legacy_id: legacy} if legacy.is_a?(Integer)
22
+ return unless value.is_a?(String) && value.ascii_only? && value.bytesize <= 256 && value.match?(/\Asc1:[A-Za-z0-9_-]+\z/)
23
+
24
+ fields = JSON.parse(Base64.urlsafe_decode64(value.delete_prefix("sc1:")))
25
+ return unless fields.is_a?(Array) && fields.length == 2
26
+ timestamp, id = fields
27
+ return unless timestamp.is_a?(String) && timestamp.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z\z/)
28
+ id = @key.parse(id)
29
+ return unless id
30
+ time = Time.iso8601(timestamp)
31
+ return unless time.year.positive? && time.utc.iso8601(6) == timestamp
32
+ {created_at: time, id: id}
33
+ rescue ArgumentError, JSON::ParserError
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AddAuth
4
+ module Core
5
+ class SessionKey
6
+ UUID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
7
+ MAX_INTEGER = (2**63) - 1
8
+
9
+ def initialize(type:)
10
+ raise ArgumentError, "unsupported session key type" unless %i[integer bigint uuid].include?(type)
11
+ @uuid = type == :uuid
12
+ end
13
+
14
+ def parse(value)
15
+ if @uuid
16
+ value.downcase if value.is_a?(String) && value.ascii_only? && UUID.match?(value)
17
+ elsif value.is_a?(Integer)
18
+ value if value.between?(1, MAX_INTEGER)
19
+ elsif value.is_a?(String) && value.ascii_only? && value.match?(/\A[1-9]\d{0,18}\z/)
20
+ integer = value.to_i
21
+ integer if integer <= MAX_INTEGER
22
+ end
23
+ end
24
+
25
+ # Stock Rails signed cookies serialize integer IDs as integers, UUIDs as
26
+ # strings. Numeric strings must not broaden the legacy integer bridge.
27
+ def legacy(value)
28
+ parse(value) if @uuid || value.is_a?(Integer)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "add_auth/core/mobile_profile"
5
+ require "add_auth/core/session_key"
6
+ require "add_auth/core/session_cursor"
5
7
 
6
8
  module AddAuth
7
9
  module Core
@@ -37,6 +39,8 @@ module AddAuth
37
39
  raise ArgumentError, "legacy_bridge_until must be an absolute Time or nil"
38
40
  end
39
41
  @store, @digest, @eligible, @clock = store, digest, eligible, clock
42
+ @key = SessionKey.new(type: store.respond_to?(:id_type) ? store.id_type : :integer)
43
+ @cursor = SessionCursor.new(key: @key)
40
44
  @access_policy = access_policy
41
45
  @password_lifecycle = password_lifecycle
42
46
  @on_sign_in = on_sign_in
@@ -191,13 +195,14 @@ module AddAuth
191
195
  def list_page(user:, current_session_id: nil, before: nil)
192
196
  empty = Page.new(entries: [])
193
197
  return empty unless user && @eligible.call(user) == true
194
- return empty unless before.nil? || before.to_s.match?(/\A[1-9]\d{0,18}\z/)
198
+ boundary = @cursor.decode(before) unless before.nil?
199
+ return empty unless before.nil? || boundary
195
200
 
196
201
  now = @clock.now
197
- candidates = @store.list_for_user(user_id: user.id, before: before&.to_i,
202
+ candidates = @store.list_for_user(user_id: user.id, before: boundary,
198
203
  excluding: current_session_id, limit: PAGE_SIZE + 1, now: now,
199
204
  active_after: now - [@idle, @remembered&.fetch(:idle_timeout) || @idle, @mobile&.idle_timeout || @idle].max, legacy: !!(@deadline && now < @deadline))
200
- cursor = candidates[PAGE_SIZE - 1].id if candidates.length > PAGE_SIZE
205
+ cursor = @cursor.encode(candidates[PAGE_SIZE - 1]) if candidates.length > PAGE_SIZE
201
206
  rows = candidates.first(PAGE_SIZE)
202
207
  if before.nil? && current_session_id
203
208
  current = @store.find_for_user(user_id: user.id, session_id: current_session_id)
@@ -217,16 +222,22 @@ module AddAuth
217
222
 
218
223
  # Both the initiating bearer and target ownership are checked under lock.
219
224
  def revoke_one(user:, session:, session_id:)
220
- return false unless session_id.is_a?(Integer) || session_id.to_s.match?(/\A[1-9]\d*\z/)
225
+ target_id = @key.parse(session_id)
226
+ return false unless target_id
221
227
 
222
228
  with_live_session(user: user, session: session) do |account, _current, now|
223
- target = @store.find_for_user_in_transaction(user_id: account.id, session_id: session_id.to_i)
229
+ target = @store.find_for_user_in_transaction(user_id: account.id, session_id: target_id)
224
230
  next false unless target
225
231
  @store.update(target, revoked_at: now) unless target.revoked_at
226
232
  true
227
233
  end || false
228
234
  end
229
235
 
236
+ def current_session?(session:, session_id:)
237
+ id = @key.parse(session_id)
238
+ !!(id && session && id == session.id)
239
+ end
240
+
230
241
  # Password proof is obtained from the locked, current account. Intake
231
242
  # limits must be applied by the caller before reaching this expensive port.
232
243
  def reauthenticate(user:, session:, purpose:, policy:)
@@ -315,10 +326,10 @@ module AddAuth
315
326
  private
316
327
 
317
328
  def cookie_lookup(value)
318
- if value.is_a?(String) && PATTERN.match?(value)
329
+ if value.is_a?(String) && value.ascii_only? && PATTERN.match?(value)
319
330
  {digest: @digest.digest(value)}
320
- elsif value.is_a?(Integer) && value.positive? && @deadline && @clock.now < @deadline
321
- {id: value}
331
+ elsif @deadline && @clock.now < @deadline && (id = @key.legacy(value))
332
+ {id: id}
322
333
  end
323
334
  end
324
335
 
@@ -7,15 +7,15 @@ module AddAuth
7
7
  module_function
8
8
 
9
9
  # Resolve when the migration runs, after the host's account table exists.
10
- # Session IDs remain integer until opaque cursor support is established.
10
+ # Keep the session keyword for migrations generated by earlier releases.
11
11
  def for(connection, table, session: false)
12
12
  key = connection.primary_key(table)
13
13
  column = connection.columns(table).find { |entry| entry.name == key }
14
14
  raise ArgumentError, "AddAuth requires a single integer or UUID primary key on #{table}" unless column
15
15
  return :bigint if column.type == :integer && column.limit == 8
16
16
  return :integer if column.type == :integer
17
- return :uuid if column.type == :uuid && !session
18
- raise ArgumentError, session ? "AddAuth requires integer Session IDs; UUID accounts can use integer Sessions" : "AddAuth requires an integer or UUID account primary key"
17
+ return :uuid if column.type == :uuid
18
+ raise ArgumentError, "AddAuth requires an integer or UUID primary key on #{table}"
19
19
  end
20
20
  end
21
21
  end
@@ -6,8 +6,11 @@ module AddAuth
6
6
  module Rails
7
7
  module Stores
8
8
  class Sessions
9
+ attr_reader :id_type
10
+
9
11
  def initialize(user_model:, session_model:)
10
12
  @users, @sessions = user_model, session_model
13
+ @id_type = @sessions.columns_hash.fetch(@sessions.primary_key).type
11
14
  raise ArgumentError, "authentication models must share one connection pool" unless @users.connection_pool.equal?(@sessions.connection_pool)
12
15
  @lock = AccountLock.new(@users)
13
16
  end
@@ -38,11 +41,16 @@ module AddAuth
38
41
  # Query cutoffs come from Core; Core still authorizes every returned row.
39
42
  def list_for_user(user_id:, before:, excluding:, limit:, now:, active_after:, legacy:)
40
43
  scope = @sessions.where(user_id: user_id, revoked_at: nil)
41
- scope = scope.where("id < ?", before) if before
44
+ if before&.key?(:legacy_id)
45
+ scope = scope.where("id < ?", before.fetch(:legacy_id))
46
+ elsif before
47
+ scope = scope.where("created_at < ? OR (created_at = ? AND id < ?)",
48
+ before.fetch(:created_at), before.fetch(:created_at), before.fetch(:id))
49
+ end
42
50
  scope = scope.where.not(id: excluding) if excluding
43
51
  scope = scope.where("(expires_at IS NULL OR expires_at > ?) AND (last_seen_at IS NULL OR last_seen_at > ?)", now, active_after)
44
52
  scope = scope.where.not(expires_at: nil).where.not(last_seen_at: nil) unless legacy
45
- scope.order(id: :desc).limit(limit).to_a
53
+ scope.order(created_at: :desc, id: :desc).limit(limit).to_a
46
54
  end
47
55
 
48
56
  def find_for_user(user_id:, session_id:) = @sessions.find_by(user_id: user_id, id: session_id)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AddAuth
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "generators/add_auth/primary_key"
4
5
  require "rails/generators/active_record"
5
6
 
6
7
  module AddAuth
7
8
  module Generators
8
9
  class AccountsGenerator < ::Rails::Generators::Base
9
10
  include ::Rails::Generators::Migration
11
+ include PrimaryKey
10
12
 
11
13
  source_root File.expand_path("templates", __dir__)
12
14
  class_option :email_link, type: :boolean, default: true, desc: "Enable email sign-in as a step-up dependency"
@@ -14,7 +14,7 @@ class AddAddAuthAccounts < ActiveRecord::Migration[8.0]
14
14
  add_column :users, :add_auth_authority, :string, default: "add_auth", null: false unless column_exists?(:users, :add_auth_authority)
15
15
  add_column :sessions, :remembered, :boolean, default: false_default, null: false unless column_exists?(:sessions, :remembered)
16
16
  add_column :sessions, :idle_timeout, :integer unless column_exists?(:sessions, :idle_timeout)
17
- create_table :add_auth_address_claims do |t|
17
+ create_table :add_auth_address_claims, id: <%= authentication_primary_key_type %> do |t|
18
18
  t.references :user, type: user_key_type, null: false, foreign_key: {on_delete: :cascade}
19
19
  t.string :digest, null: false
20
20
  t.string :state, null: false
@@ -22,7 +22,7 @@ class AddAddAuthAccounts < ActiveRecord::Migration[8.0]
22
22
  end
23
23
  add_index :add_auth_address_claims, :digest, unique: true
24
24
  add_index :add_auth_address_claims, [:user_id, :state], unique: true
25
- create_table :add_auth_account_tokens do |t|
25
+ create_table :add_auth_account_tokens, id: <%= authentication_primary_key_type %> do |t|
26
26
  t.references :user, type: user_key_type, null: false, foreign_key: {on_delete: :cascade}
27
27
  t.string :digest, null: false
28
28
  t.string :purpose, null: false
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "generators/add_auth/primary_key"
4
5
  require "rails/generators/active_record"
5
6
 
6
7
  module AddAuth
@@ -8,6 +9,7 @@ module AddAuth
8
9
  # Internal persistence foundation only. Does not install sign-in routes.
9
10
  class EmailTokensGenerator < ::Rails::Generators::Base
10
11
  include ::Rails::Generators::Migration
12
+ include PrimaryKey
11
13
 
12
14
  source_root File.expand_path("templates", __dir__)
13
15
 
@@ -2,7 +2,7 @@ require "add_auth/rails/migration/key_type"
2
2
 
3
3
  class CreateAddAuthSignInTokens < ActiveRecord::Migration[8.0]
4
4
  def change
5
- create_table :add_auth_sign_in_tokens do |t|
5
+ create_table :add_auth_sign_in_tokens, id: <%= authentication_primary_key_type %> do |t|
6
6
  t.references :user, type: AddAuth::Rails::Migration::KeyType.for(connection, :users), null: false, foreign_key: true
7
7
  t.string :digest, limit: 64, null: false
8
8
  t.string :identifier_digest, limit: 64, null: false
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "generators/add_auth/primary_key"
4
5
  require "rails/generators/active_record"
5
6
 
6
7
  module AddAuth
@@ -8,6 +9,7 @@ module AddAuth
8
9
  # Optional persistence and routes; provider strategies remain disabled until configured.
9
10
  class ExternalIdentitiesGenerator < ::Rails::Generators::Base
10
11
  include ::Rails::Generators::Migration
12
+ include PrimaryKey
11
13
 
12
14
  source_root File.expand_path("templates", __dir__)
13
15
  def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -1,8 +1,10 @@
1
+ require "add_auth/rails/migration/key_type"
2
+
1
3
  class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
2
4
  def change
3
- user_type = reference_type(:users)
4
- session_type = reference_type(:sessions)
5
- create_table :add_auth_external_identities do |t|
5
+ user_type = AddAuth::Rails::Migration::KeyType.for(connection, :users)
6
+ session_type = AddAuth::Rails::Migration::KeyType.for(connection, :sessions, session: true)
7
+ create_table :add_auth_external_identities, id: <%= authentication_primary_key_type %> do |t|
6
8
  t.references :user, type: user_type, null: false, foreign_key: {on_delete: :cascade}
7
9
  # SHA256 of the exact JSON [configuration, issuer, audience, subject].
8
10
  # Indexing this key preserves case-sensitive subjects on either store.
@@ -18,7 +20,7 @@ class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
18
20
  t.timestamps
19
21
  end
20
22
  add_index :add_auth_external_identities, :namespace, unique: true
21
- create_table :add_auth_external_transactions do |t|
23
+ create_table :add_auth_external_transactions, id: <%= authentication_primary_key_type %> do |t|
22
24
  t.string :digest, null: false
23
25
  t.string :browser_digest, null: false
24
26
  t.string :provider_id, limit: 2048, null: false
@@ -39,12 +41,4 @@ class CreateAddAuthExternalIdentities < ActiveRecord::Migration[8.0]
39
41
  add_column :sessions, :authentication_external_id, :string
40
42
  add_column :sessions, :authentication_external_version, :string
41
43
  end
42
-
43
- private
44
-
45
- def reference_type(table)
46
- column = connection.columns(table).find { |item| item.name == connection.primary_key(table) }
47
- raise "Unsupported authentication primary key" unless column && [:integer, :uuid].include?(column.type)
48
- (column.type == :integer && column.limit == 8) ? :bigint : column.type
49
- end
50
44
  end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "generators/add_auth/primary_key"
4
5
  require "rails/generators/active_record"
5
6
 
6
7
  module AddAuth
7
8
  module Generators
8
9
  class MobileSessionsGenerator < ::Rails::Generators::Base
9
10
  include ::Rails::Generators::Migration
11
+ include PrimaryKey
10
12
 
11
13
  source_root File.expand_path("templates", __dir__)
12
14
  def self.next_migration_number(dirname) = ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -1,9 +1,9 @@
1
+ require "add_auth/rails/migration/key_type"
2
+
1
3
  class CreateAddAuthMobileHandoffs < ActiveRecord::Migration[8.0]
2
4
  def change
3
- user_key = connection.columns(:users).find { |column| column.name == connection.primary_key(:users) }
4
- raise "Unsupported authentication primary key" unless user_key && [:integer, :uuid].include?(user_key.type)
5
- user_type = (user_key.type == :integer && user_key.limit == 8) ? :bigint : user_key.type
6
- create_table :add_auth_mobile_handoffs do |t|
5
+ user_type = AddAuth::Rails::Migration::KeyType.for(connection, :users)
6
+ create_table :add_auth_mobile_handoffs, id: <%= authentication_primary_key_type %> do |t|
7
7
  t.references :user, type: user_type, foreign_key: {on_delete: :cascade}
8
8
  t.string :external_digest, null: false
9
9
  t.string :digest
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "generators/add_auth/primary_key"
4
5
  require "generators/add_auth/feature_configuration"
5
6
  require "rails/generators/active_record"
6
7
 
@@ -8,6 +9,7 @@ module AddAuth
8
9
  module Generators
9
10
  class NotificationsGenerator < ::Rails::Generators::Base
10
11
  include ::Rails::Generators::Migration
12
+ include PrimaryKey
11
13
  include FeatureConfiguration
12
14
 
13
15
  source_root File.expand_path("templates", __dir__)
@@ -2,7 +2,7 @@ require "add_auth/rails/migration/key_type"
2
2
 
3
3
  class CreateAddAuthSecurityEvents < ActiveRecord::Migration[8.0]
4
4
  def change
5
- create_table :add_auth_security_events do |t|
5
+ create_table :add_auth_security_events, id: <%= authentication_primary_key_type %> do |t|
6
6
  t.references :user, type: AddAuth::Rails::Migration::KeyType.for(connection, :users), foreign_key: {on_delete: :nullify}
7
7
  t.string :kind, null: false
8
8
  t.string :digest, null: false
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "generators/add_auth/primary_key"
4
5
  require "generators/add_auth/feature_configuration"
5
6
  require "rails/generators/active_record"
6
7
 
@@ -8,6 +9,7 @@ module AddAuth
8
9
  module Generators
9
10
  class PasskeysGenerator < ::Rails::Generators::Base
10
11
  include ::Rails::Generators::Migration
12
+ include PrimaryKey
11
13
  include FeatureConfiguration
12
14
 
13
15
  source_root File.expand_path("templates", __dir__)
@@ -14,7 +14,7 @@ class AddAddAuthPasskeys < ActiveRecord::Migration[8.0]
14
14
  add_column :sessions, :authentication_policy_version, :integer, default: 0, null: false
15
15
  add_column :sessions, :authentication_credential_id, :string
16
16
  add_column :sessions, :authentication_uv, :boolean, default: false_default, null: false
17
- create_table :add_auth_credentials do |t|
17
+ create_table :add_auth_credentials, id: <%= authentication_primary_key_type %> do |t|
18
18
  t.references :user, type: user_key_type, null: false, foreign_key: {on_delete: :cascade}
19
19
  t.string :external_id, null: false
20
20
  t.text :public_key, null: false
@@ -29,7 +29,7 @@ class AddAddAuthPasskeys < ActiveRecord::Migration[8.0]
29
29
  t.timestamps
30
30
  end
31
31
  add_index :add_auth_credentials, :external_id, unique: true
32
- create_table :add_auth_ceremonies do |t|
32
+ create_table :add_auth_ceremonies, id: <%= authentication_primary_key_type %> do |t|
33
33
  t.string :digest, null: false
34
34
  t.string :challenge, null: false
35
35
  t.string :kind, null: false
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AddAuth
4
+ module Generators
5
+ module PrimaryKey
6
+ private
7
+
8
+ # Freeze Rails' generator preference into the migration source. Foreign
9
+ # keys independently follow the actual referenced table when it runs.
10
+ def authentication_primary_key_type
11
+ type = (::Rails.application.config.generators.options.dig(:active_record, :primary_key_type) || :bigint).to_s
12
+ unless %w[bigint integer uuid].include?(type)
13
+ raise Thor::Error, "AddAuth supports Rails primary_key_type :bigint, :integer or :uuid"
14
+ end
15
+ ":#{type}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -25,6 +25,9 @@ module AddAuth
25
25
  end
26
26
 
27
27
  def migration
28
+ unless Dir[File.join(destination_root, "db/migrate/*_add_add_auth_session_cursor_index.rb")].any?
29
+ migration_template "add_add_auth_session_cursor_index.rb.tt", "db/migrate/add_add_auth_session_cursor_index.rb"
30
+ end
28
31
  unless Dir[File.join(destination_root, "db/migrate/*_extend_sessions_for_add_auth.rb")].any?
29
32
  migration_template "extend_sessions_for_add_auth.rb.tt", "db/migrate/extend_sessions_for_add_auth.rb"
30
33
  end
@@ -0,0 +1,5 @@
1
+ class AddAddAuthSessionCursorIndex < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_index :sessions, [:user_id, :created_at, :id], name: "index_add_auth_session_cursor"
4
+ end
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: add_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taimoor Qureshi
@@ -329,6 +329,8 @@ files:
329
329
  - lib/add_auth/core/rate_limit.rb
330
330
  - lib/add_auth/core/remaining_factors.rb
331
331
  - lib/add_auth/core/security_events.rb
332
+ - lib/add_auth/core/session_cursor.rb
333
+ - lib/add_auth/core/session_key.rb
332
334
  - lib/add_auth/core/sessions.rb
333
335
  - lib/add_auth/core/step_up.rb
334
336
  - lib/add_auth/core/strategies/email_link.rb
@@ -423,8 +425,10 @@ files:
423
425
  - lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt
424
426
  - lib/generators/add_auth/passkeys/templates/add_auth_ceremony.rb
425
427
  - lib/generators/add_auth/passkeys/templates/add_auth_credential.rb
428
+ - lib/generators/add_auth/primary_key.rb
426
429
  - lib/generators/add_auth/session_upgrade/session_upgrade_generator.rb
427
430
  - lib/generators/add_auth/session_upgrade/templates/add_add_auth_elevation.rb.tt
431
+ - lib/generators/add_auth/session_upgrade/templates/add_add_auth_session_cursor_index.rb.tt
428
432
  - lib/generators/add_auth/session_upgrade/templates/extend_sessions_for_add_auth.rb.tt
429
433
  - lib/generators/add_auth/step_up/step_up_generator.rb
430
434
  - lib/generators/add_auth/step_up/templates/add_add_auth_reauthentication.rb.tt