Generating special tokens that are unique, tamper proof and that can store information like the purpose of the token and the token's expiry can be very useful in certain scenarios.
You can create a unique token for specific purposes like
password_reset, attach them to your application URL endpoint and send it to the user via email.
Up until now, you might have used the ActiveRecord::SignedId API that allows you to create expirable tokens. And you can query ActiveRecord to find the record using the signed id.
Consider a scenario where we want to send a password reset link to the user. If you want to generate a unique token that expires after say 1 day.
Using ActiveRecord::SignedId :
Loading development environment (Rails 7.0.3) user = User.first signed_id = user.signed_id(expires_in: 1.day, purpose: :password_reset) => "eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik16Yz0iLCJleHAiOm51bGwsInB1ciI6InVzZXIifX0=--a6aab77fee1ccc945e3fc19426799de062063e3db9b0c631a98693475a5e4b11" User.find_signed(signed_id, purpose: :password_reset) => <User id: 1 ...> user.update!(password: "new password") User.find_signed(signed_id, purpose: :password_reset) => <User id: 1 ...> # Token is still valid even after its purpose is served
Even though the purpose of the token has been served, it will stay valid until it expires.
class User < ActiveRecord::Base has_secure_password generates_token_for :password_reset, expires_in: 1.day do BCrypt::Password.new(password_digest).salt[-10..] end end
Added in Rails 7.1.0,
generates_token_for works similarly to
signed_id uses the model ID as the payload,
generates_token_for uses a combination of the model ID and whatever is returned by the block given to it. The block should return an attribute associated with the purpose of the token. Once the attribute is updated, the payload is updated as well thus invalidating the old token.
In the above example, the payload combination will be the model's ID and the last 10 characters of the password digest.
Loading development environment (Rails 7.1.0-alpha) user = User.first token = user.generate_token_for(:password_reset) => "eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaGJCMmtxU1NJUFMzbEpjMFZ4UmtKb0xnWTZCa1ZVIiwiZXhwIjpudWxsLCJwdXIiOiJVc2VyXG5wYXNzd29yZF9yZXNldFxuIn19--845ed7d939cbe28bb565334352f6ac7884ddc8d0" User.find_by_token_for(:password_reset, token) => <User id: 1 ...> user.update!(password: "new password") User.find_by_token_for(:password_reset, token) => nil
The BCrypt salt changes when the password is updated and hence the token is invalidated on the first update of the password. This makes the token available for one-time use.