standard_id 0.41.0 → 0.42.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -1
  3. data/README.md +251 -13
  4. data/app/controllers/concerns/standard_id/inertia_rendering.rb +23 -5
  5. data/app/controllers/concerns/standard_id/lifecycle_hooks.rb +13 -3
  6. data/app/controllers/concerns/standard_id/passwordless_flow.rb +11 -2
  7. data/app/controllers/concerns/standard_id/social_authentication.rb +1 -1
  8. data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +1 -8
  9. data/app/controllers/standard_id/web/login_verify_controller.rb +2 -0
  10. data/app/jobs/standard_id/password_reset_delivery_job.rb +1 -1
  11. data/app/models/concerns/standard_id/credentiable.rb +8 -1
  12. data/app/models/standard_id/application_record.rb +26 -0
  13. data/app/models/standard_id/authorization_code.rb +3 -1
  14. data/app/models/standard_id/client_application.rb +11 -0
  15. data/app/models/standard_id/identifier.rb +1 -0
  16. data/app/models/standard_id/session.rb +2 -1
  17. data/app/views/standard_id/web/login/_social_buttons.html.erb +2 -2
  18. data/app/views/standard_id/web/login/show.html.erb +5 -5
  19. data/app/views/standard_id/web/signup/show.html.erb +3 -3
  20. data/db/migrate/20250830000000_create_standard_id_client_applications.rb +2 -0
  21. data/db/migrate/20250830171553_create_standard_id_password_credentials.rb +2 -0
  22. data/db/migrate/20250830232800_create_standard_id_identifiers.rb +2 -0
  23. data/db/migrate/20250831075703_create_standard_id_credentials.rb +2 -0
  24. data/db/migrate/20250831154635_create_standard_id_sessions.rb +2 -0
  25. data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +2 -0
  26. data/db/migrate/20250903063000_create_standard_id_authorization_codes.rb +2 -0
  27. data/db/migrate/20250907090000_create_standard_id_code_challenges.rb +2 -0
  28. data/db/migrate/20260311100000_create_standard_id_refresh_tokens.rb +2 -0
  29. data/db/migrate/20260414200000_add_target_created_at_index_to_code_challenges.rb +1 -0
  30. data/db/migrate/20260416180511_add_partial_indexes_for_active_session_and_challenge_lookups.rb +25 -8
  31. data/db/migrate/20260611000000_create_standard_id_client_grants.rb +2 -0
  32. data/db/migrate/20260924000000_add_unique_active_device_index_to_standard_id_sessions.rb +115 -0
  33. data/lib/generators/standard_id/install/install_generator.rb +64 -3
  34. data/lib/generators/standard_id/install/templates/standard_id.rb +45 -15
  35. data/lib/standard_id/checks/migrations.rb +61 -0
  36. data/lib/standard_id/config/schema.rb +40 -10
  37. data/lib/standard_id/config_schema.rb +28 -5
  38. data/lib/standard_id/deprecator.rb +17 -0
  39. data/lib/standard_id/engine.rb +21 -0
  40. data/lib/standard_id/instrumentation.rb +49 -0
  41. data/lib/standard_id/migration_check.rb +183 -0
  42. data/lib/standard_id/migration_helpers.rb +65 -0
  43. data/lib/standard_id/oauth/audience_profile_resolver.rb +8 -2
  44. data/lib/standard_id/oauth/oauth_session_persistence.rb +66 -24
  45. data/lib/standard_id/oauth/refresh_token_flow.rb +23 -2
  46. data/lib/standard_id/oauth/token_grant_flow.rb +25 -3
  47. data/lib/standard_id/passwordless.rb +13 -1
  48. data/lib/standard_id/provider_registry.rb +113 -2
  49. data/lib/standard_id/providers/base.rb +222 -8
  50. data/lib/standard_id/providers/plugin_railtie.rb +59 -0
  51. data/lib/standard_id/scope_config.rb +24 -7
  52. data/lib/standard_id/testing/provider_examples.rb +117 -0
  53. data/lib/standard_id/testing.rb +1 -0
  54. data/lib/standard_id/version.rb +1 -1
  55. data/lib/standard_id.rb +26 -0
  56. metadata +24 -17
  57. data/config/initializers/migration_helpers.rb +0 -32
@@ -0,0 +1,59 @@
1
+ module StandardId
2
+ module Providers
3
+ # Namespace for the Railties generated by {Providers.plugin_railtie}.
4
+ module Railties; end
5
+
6
+ class << self
7
+ # Define the Railtie that registers a provider plugin with StandardId.
8
+ #
9
+ # Replaces the hand-written `lib/standard_id/<name>/railtie.rb` every
10
+ # provider plugin used to carry. Call it from the plugin's entry file,
11
+ # after the provider class is required:
12
+ #
13
+ # # lib/standard_id/github.rb
14
+ # require "standard_id"
15
+ # require "standard_id/github/providers/github"
16
+ #
17
+ # StandardId::Providers.plugin_railtie(:github, "StandardId::Providers::GitHub")
18
+ #
19
+ # The generated Railtie (`StandardId::Providers::Railties::<Name>`)
20
+ # registers the provider from `config.after_initialize`, once the host's
21
+ # configuration is complete. Its config fields are declared earlier, before
22
+ # `config/initializers` run, by StandardId's own engine initializer — the
23
+ # plugin has nothing further to do for that.
24
+ #
25
+ # Outside Rails (no `Rails::Railtie`) nothing is defined and nil is
26
+ # returned, so the entry file needs no `if defined?(Rails)` guard.
27
+ #
28
+ # Idempotent: calling it again for the same name returns the existing
29
+ # Railtie rather than defining a second one (which would register twice).
30
+ #
31
+ # @param name [Symbol, String] Provider identifier, as passed to
32
+ # ProviderRegistry.register (e.g. :google)
33
+ # @param provider_class [Class, String] The provider class, or its name.
34
+ # A String is constantized when the Railtie runs, so the entry file may
35
+ # call this before the class is loaded.
36
+ # @return [Class, nil] the Railtie class
37
+ def plugin_railtie(name, provider_class)
38
+ return nil unless defined?(::Rails::Railtie)
39
+
40
+ const_name = name.to_s.camelize
41
+ if Railties.const_defined?(const_name, false)
42
+ return Railties.const_get(const_name, false)
43
+ end
44
+
45
+ provider_name = name.to_sym
46
+ railtie = Class.new(::Rails::Railtie)
47
+ Railties.const_set(const_name, railtie)
48
+
49
+ railtie.config.after_initialize do
50
+ klass = provider_class.is_a?(String) ? provider_class.constantize : provider_class
51
+ StandardId::ProviderRegistry.register(provider_name, klass)
52
+ Rails.logger&.debug("[StandardId] registered provider #{provider_name}")
53
+ end
54
+
55
+ railtie
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,7 +1,11 @@
1
1
  module StandardId
2
2
  class ScopeConfig
3
3
  # @!attribute [r] allow_registration
4
- # Reserved for future use — controls whether new accounts can register under this scope.
4
+ # Whether passwordless sign-in under this scope may create a new account
5
+ # (default true). It can only RESTRICT: registration happens iff the
6
+ # global switch allows it (web.passwordless_registration for the
7
+ # WebEngine; the caller's `allow_registration:` for host controllers using
8
+ # StandardId::PasswordlessFlow) AND this is true. See #allow_registration?.
5
9
  # @!attribute [r] profile_types
6
10
  # Array of profile-type class names accepted by this scope. Any profile matching any of
7
11
  # these types satisfies the built-in profile check.
@@ -20,11 +24,9 @@ module StandardId
20
24
  :allow_registration,
21
25
  :authorizer
22
26
 
23
- # Shared deprecator instance. Creating a new ActiveSupport::Deprecation on
24
- # every extract_profile_types call bypasses the host app's configured
25
- # deprecation behaviour (Rails 7.1+ routes through deprecation registries)
26
- # and allocates for every scope init. One instance is enough.
27
- DEPRECATOR = ActiveSupport::Deprecation.new("2.0", "StandardId")
27
+ # Kept as an alias for hosts/specs that reference it; it is the gem-wide
28
+ # StandardId.deprecator, registered in Rails.application.deprecators.
29
+ DEPRECATOR = StandardId.deprecator
28
30
 
29
31
  # Normalize profile-type inputs from config.
30
32
  #
@@ -59,7 +61,8 @@ module StandardId
59
61
  @after_sign_in_path = config[:after_sign_in_path]
60
62
  @no_profile_message = config[:no_profile_message] || default_no_profile_message
61
63
  @label = config[:label] || name.to_s.humanize
62
- @allow_registration = config.fetch(:allow_registration, true)
64
+ # A present-but-nil key means "not configured", i.e. the default (true).
65
+ @allow_registration = config[:allow_registration].nil? ? true : config[:allow_registration]
63
66
  @authorizer = config[:authorizer]
64
67
  end
65
68
 
@@ -82,6 +85,20 @@ module StandardId
82
85
  authorizer.respond_to?(:call)
83
86
  end
84
87
 
88
+ def allow_registration?
89
+ allow_registration != false
90
+ end
91
+
92
+ # Combine a global/caller registration switch with a (possibly nil) scope.
93
+ # No scope → the global value unchanged; a scope can only turn it off.
94
+ #
95
+ # @param global [Boolean]
96
+ # @param scope_config [StandardId::ScopeConfig, nil]
97
+ def self.registration_allowed?(global, scope_config)
98
+ return false unless global
99
+ scope_config.nil? || scope_config.allow_registration?
100
+ end
101
+
85
102
  private
86
103
 
87
104
  def default_no_profile_message
@@ -0,0 +1,117 @@
1
+ # RSpec support for host apps (and provider plugins) that want to pin down
2
+ # "this provider plugin is installed, registered, and its config fields are
3
+ # writable from config/initializers/standard_id.rb".
4
+ #
5
+ # Loaded automatically by `require "standard_id/testing"` when RSpec is
6
+ # present, or explicitly:
7
+ #
8
+ # require "standard_id/testing/provider_examples"
9
+ #
10
+ # Shared example — the one-liner most apps want:
11
+ #
12
+ # RSpec.describe "StandardId social providers" do
13
+ # it_behaves_like "a registered StandardId provider", :google
14
+ # it_behaves_like "a registered StandardId provider", :apple,
15
+ # config_fields: %i[apple_client_id apple_private_key apple_key_id apple_team_id]
16
+ # end
17
+ #
18
+ # `config_fields:` defaults to every field in the provider's `config_schema`.
19
+ #
20
+ # Matcher — for ad-hoc assertions:
21
+ #
22
+ # expect(:google).to be_a_registered_standard_id_provider
23
+ # expect(:apple).to be_a_registered_standard_id_provider.with_config_fields(:apple_client_id, :apple_team_id)
24
+ #
25
+ require "standard_id"
26
+
27
+ module StandardId
28
+ module Testing
29
+ module ProviderExamples
30
+ module_function
31
+
32
+ # Problems preventing `name` from counting as a registered provider with
33
+ # the given social config fields. Empty when all is well.
34
+ #
35
+ # @param name [Symbol, String]
36
+ # @param fields [Array<Symbol>, nil] nil = the provider's config_schema keys
37
+ # @return [Array<String>]
38
+ def problems(name, fields = nil)
39
+ return ["provider #{name.inspect} is not registered with StandardId::ProviderRegistry"] unless StandardId::ProviderRegistry.registered?(name)
40
+
41
+ fields = config_fields_for(name) if fields.nil?
42
+ fields.filter_map do |field|
43
+ next "#{field} is not declared on the social config scope" unless StandardId::ConfigSchema.instance.field?(:social, field)
44
+
45
+ begin
46
+ StandardId.config.social.public_send(field)
47
+ nil
48
+ rescue StandardError => e
49
+ "reading social.#{field} raised #{e.class}: #{e.message}"
50
+ end
51
+ end
52
+ end
53
+
54
+ def config_fields_for(name)
55
+ StandardId::ProviderRegistry.get(name).config_schema.keys.map(&:to_sym)
56
+ end
57
+
58
+ # Write `value` through the same setter a host initializer uses, then
59
+ # restore the field exactly — including "never assigned", so an ENV
60
+ # fallback keeps working for later examples.
61
+ def round_trip(field, value)
62
+ social = StandardId.config.social
63
+ assigned = social.key?(field.to_sym)
64
+ original = social.to_h[field.to_sym]
65
+ social.public_send(:"#{field}=", value)
66
+ social.public_send(field)
67
+ ensure
68
+ if assigned
69
+ social[field.to_sym] = original
70
+ else
71
+ social.delete(field.to_sym)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ if defined?(RSpec::Matchers) && RSpec::Matchers.respond_to?(:define)
79
+ RSpec::Matchers.define :be_a_registered_standard_id_provider do
80
+ chain(:with_config_fields) { |*fields| @fields = fields.flatten.map(&:to_sym) }
81
+
82
+ match do |name|
83
+ @problems = StandardId::Testing::ProviderExamples.problems(name, @fields)
84
+ @problems.empty?
85
+ end
86
+
87
+ failure_message do |name|
88
+ "expected #{name.inspect} to be a registered StandardId provider, but:\n " + @problems.join("\n ")
89
+ end
90
+
91
+ failure_message_when_negated do |name|
92
+ "expected #{name.inspect} not to be a registered StandardId provider"
93
+ end
94
+ end
95
+ end
96
+
97
+ if defined?(RSpec) && RSpec.respond_to?(:shared_examples)
98
+ RSpec.shared_examples "a registered StandardId provider" do |name, config_fields: nil|
99
+ fields_for = -> { config_fields || StandardId::Testing::ProviderExamples.config_fields_for(name) }
100
+
101
+ it "registers the #{name} provider" do
102
+ expect(StandardId::ProviderRegistry.registered?(name)).to be(true),
103
+ "#{name.inspect} is not registered — is its plugin gem in the Gemfile, and did the app boot its Railtie?"
104
+ end
105
+
106
+ it "declares the #{name} config fields on the social scope" do
107
+ expect(name).to be_a_registered_standard_id_provider.with_config_fields(fields_for.call)
108
+ end
109
+
110
+ it "accepts writes to the #{name} config fields, as config/initializers/standard_id.rb does" do
111
+ fields_for.call.each do |field|
112
+ expect(StandardId::Testing::ProviderExamples.round_trip(field, "probe-value")).to eq("probe-value"),
113
+ "writing social.#{field} did not round-trip"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -1,5 +1,6 @@
1
1
  require "standard_id/testing/authentication_helpers"
2
2
  require "standard_id/testing/request_helpers"
3
+ require "standard_id/testing/provider_examples" if defined?(RSpec)
3
4
 
4
5
  module StandardId
5
6
  module Testing
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.41.0"
2
+ VERSION = "0.42.0"
3
3
  end
data/lib/standard_id.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require "standard_id/version"
2
+ require "standard_id/deprecator"
3
+ require "standard_id/instrumentation"
2
4
  require "standard_id/current_attributes"
3
5
  require "standard_id/engine"
4
6
  require "standard_id/web_engine"
@@ -7,6 +9,9 @@ require "standard_id/config_schema"
7
9
  require "standard_id/config/schema"
8
10
  require "standard_id/scope_config"
9
11
  require "standard_id/association_strict_loading"
12
+ require "standard_id/migration_check"
13
+ require "standard_id/migration_helpers"
14
+ require "standard_id/checks/migrations"
10
15
  require "standard_id/errors"
11
16
  require "standard_id/events"
12
17
  require "standard_id/events/subscribers/base"
@@ -65,6 +70,7 @@ require "concurrent/delay"
65
70
 
66
71
  require "standard_id/providers/base"
67
72
  require "standard_id/provider_registry"
73
+ require "standard_id/providers/plugin_railtie"
68
74
 
69
75
  module StandardId
70
76
  CONFIG = Concurrent::Delay.new { ConfigSchema.build }
@@ -105,6 +111,26 @@ module StandardId
105
111
  ScopeConfig.new(name, scope_hash)
106
112
  end
107
113
 
114
+ # Registered social providers the host app has switched on.
115
+ #
116
+ # @return [Hash{String => Class}] Provider name => provider class
117
+ # @see StandardId::Providers::Base.enabled?
118
+ def enabled_social_providers
119
+ ProviderRegistry.enabled
120
+ end
121
+
122
+ # Whether the named social provider is registered AND switched on.
123
+ #
124
+ # Safe to call for a provider whose plugin gem is not installed — returns
125
+ # false rather than raising. Prefer this over checking a client ID
126
+ # directly (`StandardId.config.google_client_id.present?`).
127
+ #
128
+ # @param name [Symbol, String] e.g. :google
129
+ # @return [Boolean]
130
+ def social_provider_enabled?(name)
131
+ ProviderRegistry.registered?(name) && ProviderRegistry.get(name).enabled?
132
+ end
133
+
108
134
  def skip_host_authorization(framework: nil, callback: nil)
109
135
  AuthorizationBypass.apply(framework: framework, callback: callback)
110
136
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.41.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -57,20 +57,6 @@ dependencies:
57
57
  - - "<"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '4'
60
- - !ruby/object:Gem::Dependency
61
- name: ostruct
62
- requirement: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - ">="
65
- - !ruby/object:Gem::Version
66
- version: '0'
67
- type: :runtime
68
- prerelease: false
69
- version_requirements: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - ">="
72
- - !ruby/object:Gem::Version
73
- version: '0'
74
60
  - !ruby/object:Gem::Dependency
75
61
  name: concurrent-ruby
76
62
  requirement: !ruby/object:Gem::Requirement
@@ -99,6 +85,20 @@ dependencies:
99
85
  - - "~>"
100
86
  - !ruby/object:Gem::Version
101
87
  version: '6.5'
88
+ - !ruby/object:Gem::Dependency
89
+ name: ostruct
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
102
  description: StandardId is an authentication engine that provides a complete, secure-by-default
103
103
  solution for identity management, reducing boilerplate and eliminating common security
104
104
  pitfalls.
@@ -213,7 +213,6 @@ files:
213
213
  - app/views/standard_id/web/signup/show.html.erb
214
214
  - config/brakeman.ignore
215
215
  - config/initializers/generators.rb
216
- - config/initializers/migration_helpers.rb
217
216
  - config/routes/api.rb
218
217
  - config/routes/web.rb
219
218
  - db/migrate/20250830000000_create_standard_id_client_applications.rb
@@ -231,6 +230,7 @@ files:
231
230
  - db/migrate/20260416180511_add_partial_indexes_for_active_session_and_challenge_lookups.rb
232
231
  - db/migrate/20260611000000_create_standard_id_client_grants.rb
233
232
  - db/migrate/20260915000000_remove_refresh_token_lifetime_from_standard_id_client_applications.rb
233
+ - db/migrate/20260924000000_add_unique_active_device_index_to_standard_id_sessions.rb
234
234
  - lib/generators/standard_id/install/install_generator.rb
235
235
  - lib/generators/standard_id/install/templates/standard_id.rb
236
236
  - lib/standard_id.rb
@@ -243,11 +243,13 @@ files:
243
243
  - lib/standard_id/association_strict_loading.rb
244
244
  - lib/standard_id/authorization_bypass.rb
245
245
  - lib/standard_id/bearer_token_extraction.rb
246
+ - lib/standard_id/checks/migrations.rb
246
247
  - lib/standard_id/config/callable_validator.rb
247
248
  - lib/standard_id/config/schema.rb
248
249
  - lib/standard_id/config/scope_claims_validator.rb
249
250
  - lib/standard_id/config_schema.rb
250
251
  - lib/standard_id/current_attributes.rb
252
+ - lib/standard_id/deprecator.rb
251
253
  - lib/standard_id/engine.rb
252
254
  - lib/standard_id/errors.rb
253
255
  - lib/standard_id/events.rb
@@ -260,7 +262,10 @@ files:
260
262
  - lib/standard_id/events/subscribers/password_reset_delivery_subscriber.rb
261
263
  - lib/standard_id/events/subscribers/passwordless_delivery_subscriber.rb
262
264
  - lib/standard_id/http_client.rb
265
+ - lib/standard_id/instrumentation.rb
263
266
  - lib/standard_id/jwt_service.rb
267
+ - lib/standard_id/migration_check.rb
268
+ - lib/standard_id/migration_helpers.rb
264
269
  - lib/standard_id/oauth/audience_profile_resolver.rb
265
270
  - lib/standard_id/oauth/audience_scope_resolver.rb
266
271
  - lib/standard_id/oauth/authorization_code_authorization_flow.rb
@@ -291,6 +296,7 @@ files:
291
296
  - lib/standard_id/passwordless/verification_service.rb
292
297
  - lib/standard_id/provider_registry.rb
293
298
  - lib/standard_id/providers/base.rb
299
+ - lib/standard_id/providers/plugin_railtie.rb
294
300
  - lib/standard_id/rate_limit_store.rb
295
301
  - lib/standard_id/routing.rb
296
302
  - lib/standard_id/scope_config.rb
@@ -302,6 +308,7 @@ files:
302
308
  - lib/standard_id/testing/factories/oauth.rb
303
309
  - lib/standard_id/testing/factories/sessions.rb
304
310
  - lib/standard_id/testing/factory_bot.rb
311
+ - lib/standard_id/testing/provider_examples.rb
305
312
  - lib/standard_id/testing/request_helpers.rb
306
313
  - lib/standard_id/utils/callable_parameter_filter.rb
307
314
  - lib/standard_id/utils/ip_normalizer.rb
@@ -336,5 +343,5 @@ requirements: []
336
343
  rubygems_version: 4.0.3
337
344
  specification_version: 4
338
345
  summary: A comprehensive authentication engine for Rails, built on the security primitives
339
- introduced in Rails 7/8.
346
+ introduced in Rails 8.
340
347
  test_files: []
@@ -1,32 +0,0 @@
1
- # Monkey patch ActiveRecord::Migration to add primary_key_type and foreign_key_type methods
2
- module ActiveRecord
3
- class Migration
4
- class << self
5
- def primary_and_foreign_key_types
6
- config = Rails.configuration.generators
7
- config.options[config.orm][:primary_key_type] || :bigint
8
- end
9
-
10
- def primary_key_type
11
- primary_and_foreign_key_types
12
- end
13
-
14
- def foreign_key_type
15
- primary_and_foreign_key_types
16
- end
17
- end
18
-
19
- # Make these methods available as instance methods too
20
- def primary_and_foreign_key_types
21
- self.class.primary_and_foreign_key_types
22
- end
23
-
24
- def primary_key_type
25
- self.class.primary_key_type
26
- end
27
-
28
- def foreign_key_type
29
- self.class.foreign_key_type
30
- end
31
- end
32
- end