Skip to content

Commit 7645f01

Browse files
Merge pull request rails#54422 from AliSepehri/use-message-verifiers-for-signed-id
Refactor ActiveRecord Signed ID to use global `Rails.application.message_verifiers`
2 parents 1dc5019 + e4218ff commit 7645f01

File tree

7 files changed

+333
-57
lines changed

7 files changed

+333
-57
lines changed

activerecord/CHANGELOG.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,83 @@
1+
* Allow signed ID verifiers to be configurable via `Rails.application.message_verifiers`
2+
3+
Prior to this change, the primary way to configure signed ID verifiers was
4+
to set `signed_id_verifier` on each model class:
5+
6+
```ruby
7+
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
8+
Comment.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
9+
```
10+
11+
And if the developer did not set `signed_id_verifier`, a verifier would be
12+
instantiated with a secret derived from `secret_key_base` and the following
13+
options:
14+
15+
```ruby
16+
{ digest: "SHA256", serializer: JSON, url_safe: true }
17+
```
18+
19+
Thus it was cumbersome to rotate configuration for all verifiers.
20+
21+
This change defines a new Rails config: [`config.active_record.use_legacy_signed_id_verifier`][].
22+
The default value is `:generate_and_verify`, which preserves the previous
23+
behavior. However, when set to `:verify`, signed ID verifiers will use
24+
configuration from `Rails.application.message_verifiers` (specifically,
25+
`Rails.application.message_verifiers["active_record/signed_id"]`) to
26+
generate and verify signed IDs, but will also verify signed IDs using the
27+
older configuration.
28+
29+
To avoid complication, the new behavior only applies when `signed_id_verifier_secret`
30+
is not set on a model class or any of its ancestors. Additionally,
31+
`signed_id_verifier_secret` is now deprecated. If you are currently setting
32+
`signed_id_verifier_secret` on a model class, you can set `signed_id_verifier`
33+
instead:
34+
35+
```ruby
36+
# BEFORE
37+
Post.signed_id_verifier_secret = "my secret"
38+
39+
# AFTER
40+
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("my secret", digest: "SHA256", serializer: JSON, url_safe: true)
41+
```
42+
43+
To ease migration, `signed_id_verifier` has also been changed to behave as a
44+
`class_attribute` (i.e. inheritable), but _only when `signed_id_verifier_secret`
45+
is not set_:
46+
47+
```ruby
48+
# BEFORE
49+
ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
50+
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false
51+
52+
# AFTER
53+
ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
54+
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => true
55+
56+
Post.signed_id_verifier_secret = "my secret" # => deprecation warning
57+
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false
58+
```
59+
60+
Note, however, that it is recommended to eventually migrate from
61+
model-specific verifiers to a unified configuration managed by
62+
`Rails.application.message_verifiers`. `ActiveSupport::MessageVerifier#rotate`
63+
can facilitate that transition. For example:
64+
65+
```ruby
66+
# BEFORE
67+
# Generate and verify signed Post IDs using Post-specific configuration
68+
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("post secret", ...)
69+
70+
# AFTER
71+
# Generate and verify signed Post IDs using the unified configuration
72+
Post.signed_id_verifier = Post.signed_id_verifier.dup
73+
# Fall back to Post-specific configuration when verifying signed IDs
74+
Post.signed_id_verifier.rotate("post secret", ...)
75+
```
76+
77+
[`config.active_record.use_legacy_signed_id_verifier`]: https://guides.rubyonrails.org/v8.1/configuring.html#config-active-record-use-legacy-signed-id-verifier
78+
79+
*Ali Sepehri*, *Jonathan Hefner*
80+
181
* Prepend `extra_flags` in postgres' `structure_load`
282
383
When specifying `structure_load_flags` with a postgres adapter, the flags

activerecord/lib/active_record.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,13 @@ def self.marshalling_format_version=(value)
506506
}
507507
)
508508

509+
##
510+
# :singleton-method: message_verifiers
511+
#
512+
# ActiveSupport::MessageVerifiers instance for Active Record. If you are using
513+
# Rails, this will be set to +Rails.application.message_verifiers+.
514+
singleton_class.attr_accessor :message_verifiers
515+
509516
def self.eager_load!
510517
super
511518
ActiveRecord::Locking.eager_load!

activerecord/lib/active_record/railtie.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Railtie < Rails::Railtie # :nodoc:
3838
config.active_record.raise_on_assign_to_attr_readonly = false
3939
config.active_record.belongs_to_required_validates_foreign_key = true
4040
config.active_record.generate_secure_token_on = :create
41+
config.active_record.use_legacy_signed_id_verifier = :generate_and_verify
4142

4243
config.active_record.queues = ActiveSupport::InheritableOptions.new
4344

@@ -233,6 +234,7 @@ class Railtie < Rails::Railtie # :nodoc:
233234
:check_schema_cache_dump_version,
234235
:use_schema_cache_dump,
235236
:postgresql_adapter_decode_dates,
237+
:use_legacy_signed_id_verifier,
236238
)
237239

238240
configs_used_in_other_initializers.each do |k, v|
@@ -319,9 +321,18 @@ class Railtie < Rails::Railtie # :nodoc:
319321
end
320322
end
321323

322-
initializer "active_record.set_signed_id_verifier_secret" do
323-
ActiveSupport.on_load(:active_record) do
324-
self.signed_id_verifier_secret ||= -> { Rails.application.key_generator.generate_key("active_record/signed_id") }
324+
initializer "active_record.configure_message_verifiers" do |app|
325+
ActiveRecord.message_verifiers = app.message_verifiers
326+
327+
use_legacy_signed_id_verifier = app.config.active_record.use_legacy_signed_id_verifier
328+
legacy_options = { digest: "SHA256", serializer: JSON, url_safe: true }
329+
330+
if use_legacy_signed_id_verifier == :generate_and_verify
331+
app.message_verifiers.prepend { |salt| legacy_options if salt == "active_record/signed_id" }
332+
elsif use_legacy_signed_id_verifier == :verify
333+
app.message_verifiers.rotate { |salt| legacy_options if salt == "active_record/signed_id" }
334+
elsif use_legacy_signed_id_verifier
335+
raise ArgumentError, "Unrecognized value for config.active_record.use_legacy_signed_id_verifier: #{use_legacy_signed_id_verifier.inspect}"
325336
end
326337
end
327338

activerecord/lib/active_record/signed_id.rb

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,27 @@ module SignedId
66
extend ActiveSupport::Concern
77

88
included do
9+
class_attribute :_signed_id_verifier, instance_accessor: false, instance_predicate: false
10+
911
##
1012
# :singleton-method:
1113
# Set the secret used for the signed id verifier instance when using Active Record outside of \Rails.
1214
# Within \Rails, this is automatically set using the \Rails application key generator.
1315
class_attribute :signed_id_verifier_secret, instance_writer: false
16+
module DeprecateSignedIdVerifierSecret
17+
def signed_id_verifier_secret=(secret)
18+
ActiveRecord.deprecator.warn(<<~MSG)
19+
ActiveRecord::Base.signed_id_verifier_secret is deprecated and will be removed in the future.
20+
21+
If the secret is model-specific, set Model.signed_id_verifier instead.
22+
23+
Otherwise, configure Rails.application.message_verifiers (or ActiveRecord.message_verifiers) with the secret.
24+
MSG
25+
26+
super
27+
end
28+
end
29+
singleton_class.prepend DeprecateSignedIdVerifierSecret
1430
end
1531

1632
module RelationMethods # :nodoc:
@@ -77,28 +93,38 @@ def find_signed!(signed_id, purpose: nil, on_rotation: nil)
7793
end
7894
end
7995

80-
# The verifier instance that all signed ids are generated and verified from. By default, it'll be initialized
81-
# with the class-level +signed_id_verifier_secret+, which within Rails comes from
82-
# {Rails.application.key_generator}[rdoc-ref:Rails::Application#key_generator].
83-
# By default, it's SHA256 for the digest and JSON for the serialization.
8496
def signed_id_verifier
85-
@signed_id_verifier ||= begin
86-
secret = signed_id_verifier_secret
87-
secret = secret.call if secret.respond_to?(:call)
97+
if signed_id_verifier_secret
98+
@signed_id_verifier ||= begin
99+
secret = signed_id_verifier_secret
100+
secret = secret.call if secret.respond_to?(:call)
101+
102+
if secret.nil?
103+
raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs"
104+
end
88105

89-
if secret.nil?
90-
raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids"
91-
else
92106
ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
93107
end
108+
else
109+
return _signed_id_verifier if _signed_id_verifier
110+
111+
if ActiveRecord.message_verifiers.nil?
112+
raise "You must set ActiveRecord.message_verifiers to use signed IDs"
113+
end
114+
115+
ActiveRecord.message_verifiers["active_record/signed_id"]
94116
end
95117
end
96118

97119
# Allows you to pass in a custom verifier used for the signed ids. This also allows you to use different
98120
# verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare
99121
# your custom verifier for that in advance. See ActiveSupport::MessageVerifier for details.
100122
def signed_id_verifier=(verifier)
101-
@signed_id_verifier = verifier
123+
if signed_id_verifier_secret
124+
@signed_id_verifier = verifier
125+
else
126+
self._signed_id_verifier = verifier
127+
end
102128
end
103129

104130
# :nodoc:

0 commit comments

Comments
 (0)