Starting with version 6.1, ActiveRecord will support looking for records using signed IDs. This feature can be handy when doing things like resetting passwords, sending invitations, or doing email verification.
Signed IDs have three characteristics:
- They are tamper-proof
- They can be set to expire
- They can be scoped to a particular purpose
How is this different from GlobalId?
The above seems oddly similar to Signed Global IDs, so you may be wondering, why was this feature introduced?
The main difference is that ActiveRecord signed IDs can only be used when referring to a single concrete class, conversely GlobalId can be used when the passed ID might respond to any number of classes.
How can I use signed IDs?
When Rails 6.1 is released all ActiveRecord instances will respond to:
-
signed_idto generate the token -
find_signedto find a record using the token -
find_signed!same as the above but instead of returningnilwhen the record is not found an error will be raised
Here's a simple example:
signed_user_id = User.first.signed_id
User.find_signed(signed_user_id)
=> User.first
If you want to set a purpose and expiration to the token you can pass them as arguments to signed_id, like this:
signed_user_id = User.first.signed_id(purpose: :email_confirmation, expires_in: 30.minutes.from_now)
User.find_signed(signed_user_id)
=> nil
User.find_signed(signed_user_id, purpose: :other)
=> nil
User.find_signed(signed_user_id, purpose: : email_confirmation)
=> User.first
If you don't want the query to silently fail, you can use the bang-method alternative:
User.find_signed!("invalid_token")
=> ActiveSupport::MessageVerifier::InvalidSignature
Rails will automatically generate a signed_id_verifier_secret to be used when encoding and decoding the tokens. You can also provide your own in an initializer.
# config/initializers/active_record.rb
ActiveRecord::Base.signed_id_verifier_secret = "custom_verfifier_secret"
If you are interested in learning how this was implemented, take a look at the pull request where this feature was introduced.
Top comments (2)
For the
expirationparameter you should not be using.from_now(Rails 7). Instead, you should be calling it this way:expiration: 1.dayorexpiration: 10.minutesCan they be used as an alternative to JWT for user authentication?