Module: ActiveRecord::SignedId::ClassMethods

Defined in:
activerecord/lib/active_record/signed_id.rb

Instance Method Summary collapse

Instance Method Details

#combine_signed_id_purposes(purpose) ⇒ Object

:nodoc:



131
132
133
# File 'activerecord/lib/active_record/signed_id.rb', line 131

def combine_signed_id_purposes(purpose)
  [ base_class.name.underscore, purpose.to_s ].compact_blank.join("/")
end

#find_signed(signed_id, purpose: nil, on_rotation: nil) ⇒ Object

Lets you find a record based on a signed id that’s safe to put into the world without risk of tampering. This is particularly useful for things like password reset or email verification, where you want the bearer of the signed id to be able to interact with the underlying record, but usually only within a certain time period.

You set the time period that the signed id is valid for during generation, using the instance method signed_id(expires_in: 15.minutes). If the time has elapsed before a signed find is attempted, the signed id will no longer be valid, and nil is returned.

It’s possible to further restrict the use of a signed id with a purpose. This helps when you have a general base model, like a User, which might have signed ids for several things, like password reset or email verification. The purpose that was set during generation must match the purpose set when finding. If there’s a mismatch, nil is again returned.

Examples

signed_id = User.first.signed_id expires_in: 15.minutes, purpose: :password_reset

User.find_signed signed_id # => nil, since the purpose does not match

travel 16.minutes
User.find_signed signed_id, purpose: :password_reset # => nil, since the signed id has expired

travel_back
User.find_signed signed_id, purpose: :password_reset # => User.first

Raises:



68
69
70
71
72
73
74
75
# File 'activerecord/lib/active_record/signed_id.rb', line 68

def find_signed(signed_id, purpose: nil, on_rotation: nil)
  raise UnknownPrimaryKey.new(self) if primary_key.nil?

  options = { on_rotation: on_rotation }.compact
  if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
    find_by primary_key => id
  end
end

#find_signed!(signed_id, purpose: nil, on_rotation: nil) ⇒ Object

Works like find_signed, but will raise an ActiveSupport::MessageVerifier::InvalidSignature exception if the signed_id has either expired, has a purpose mismatch, is for another record, or has been tampered with. It will also raise an ActiveRecord::RecordNotFound exception if the valid signed id can’t find a record.

Examples

User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature

signed_id = User.first.signed_id
User.first.destroy
User.find_signed! signed_id # => ActiveRecord::RecordNotFound


89
90
91
92
93
94
# File 'activerecord/lib/active_record/signed_id.rb', line 89

def find_signed!(signed_id, purpose: nil, on_rotation: nil)
  options = { on_rotation: on_rotation }.compact
  if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
    find(id)
  end
end

#signed_id_verifierObject



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'activerecord/lib/active_record/signed_id.rb', line 96

def signed_id_verifier
  if signed_id_verifier_secret
    @signed_id_verifier ||= begin
      secret = signed_id_verifier_secret
      secret = secret.call if secret.respond_to?(:call)

      if secret.nil?
        raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs"
      end

      ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
    end
  else
    return _signed_id_verifier if _signed_id_verifier

    if ActiveRecord.message_verifiers.nil?
      raise "You must set ActiveRecord.message_verifiers to use signed IDs"
    end

    ActiveRecord.message_verifiers["active_record/signed_id"]
  end
end

#signed_id_verifier=(verifier) ⇒ Object

Allows you to pass in a custom verifier used for the signed ids. This also allows you to use different verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare your custom verifier for that in advance. See ActiveSupport::MessageVerifier for details.



122
123
124
125
126
127
128
# File 'activerecord/lib/active_record/signed_id.rb', line 122

def signed_id_verifier=(verifier)
  if signed_id_verifier_secret
    @signed_id_verifier = verifier
  else
    self._signed_id_verifier = verifier
  end
end