DEV Community

Sebastian Sogamoso
Sebastian Sogamoso

Posted on

Rails signed IDs for ActiveRecord

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_id to generate the token
  • find_signed to find a record using the token
  • find_signed! same as the above but instead of returning nil when the record is not found an error will be raised

Here's a simple example:

signed_user_id = User.first.signed_id

=> User.first
Enter fullscreen mode Exit fullscreen mode

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)

=> nil

User.find_signed(signed_user_id, purpose: :other)
=> nil

User.find_signed(signed_user_id, purpose: : email_confirmation)
=> User.first
Enter fullscreen mode Exit fullscreen mode

If you don't want the query to silently fail, you can use the bang-method alternative:

=> ActiveSupport::MessageVerifier::InvalidSignature
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)

spaquet profile image
Stephane Paquet

For the expiration parameter you should not be using .from_now (Rails 7). Instead, you should be calling it this way: expiration: or expiration: 10.minutes

uxmansherwani profile image
Usman Sherwani

Can they be used as an alternative to JWT for user authentication?