Following on from this earlier post documenting the implementation process of Rails 8 generated authentication served via API to a React front end, where I did not implement or discuss in detail password reset, this post looks into the Rails 8 password reset mechanism in more detail.
Rails 8 auth gives us a fully signed, expirable, purpose-limited reset token system, without us having to write a single line of token logic ourselves, thank you the Rails team 👏.
🔑 ✉️ The password reset stages in Rails 8 auth:
➡️ Password reset button on the login form routes to PasswordController #new and renders the new.html.erb template, containing an email input form and an 'email password reset instructions' button.
➡️ new form submits to PasswordsController #create, finds db User record from the email param, creates a Mailer Job by calling deliver_later on PasswordsMailer.reset(user).
➡️ Job emails the mailers/reset.html.erb mailer to the user, which contains a link, the URL of which is embedded with a unique token generated from the users id by the @user.password_reset_token call in the reset mailer.
➡️ user clicks the link containing token param, routes to PasswordsController #edit, renders the edit.thml.erb template, which contains a form with new password and password_confirmation fields. #edit action also calls the #set_user_from_token (courtesy of a before_action for #edit & #update) method with the token param, which decrypts & validates the token and sets @user.
➡️ submission of the form in the edit password template, with the token again passed as a param, calls the PasswordController #update action which if #set_user_from_token, courtesy again of before_action, decrypts, validates and sets to a valid @user then updates the user model record with the new password. Password update is then completed, user routed back to the new_session_path for login.
Token Magic
Token generation and decoding are the core of this flow. Here’s how it works behind the scenes.
🔏 How is the token created?:
- The token is created by this value passed to the url helper within the reset mailer's link_to:
@user.password_reset_token
#password_reset_token is syntactic sugar for #generate_token_for(:password_reset). This code is from the ActiveRecord::Tokens module which from Rails 8 is included in ActiveRecord::Base. This includes the ability to both generate signed, expirable, purpose-scoped tokens, as well as to securely verify and decode them (itself utilising ActiveSupport::MessageVerifier as the cryptographic engine). As a result, methods like user.password_reset_token and user.find_by_password_reset_token!(_token_) are available out of the box. See final section for in depth discussion of these methods.
The url in the link_to in the reset mailer:
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>
generates a URL like /passwords/:token/edit(.:format) due to this route declaration
resources :passwords, param: :token`
This sets up a route pattern where :token replaces the usual :id, so the generated URL includes the generated reset token in place of :token.
🧩 What does the actual token look like?:
The actual token generated will be a variation of this two part string, -- ;
eyJfcmFpbHMiOnsiZGF0YSI6WzYsIlNJdDV5RzIuU3UiXSwiZXhwIjoiMjAyNS0wNi0wNlQyMDo0MjoxNS4xMjFaIiwicHVyIjoiVXNlclxucGFzc3dvcmRfcmVzZXRcbjkwMCJ9fQ==--4d759ab766988553b56740f7ad1386d05a1e5410
1st part – the payload
The payload will be a Base64 encoded representation of a JSON has like the below:
{
_rails: {
data: [user.id, user.class.signed_id_verifier.verifier_name], # usually [6, "SomeInternalSalt"]
exp: 15.minutes.from_now.utc.iso8601, # e.g. "2025-06-06T20:42:15.121Z"
pur: "User\npassword_reset\n900" # class name, purpose, expiry in seconds
}
}
2nd part - the HMAC digest:
HMAC stands for Hash-based Message Authentication Code. It’s a cryptographic technique that:
- takes a message (in this case, the Base64-encoded token payload).
- combines it with a secret key (uses your secret_key_base).
- produces a fixed-length hash output.
🔓 How is the token verified?:
PasswordsController has a before action which calls #set_user_by_token for both the #edit and #update actions. This private method takes the token from the params and calls User.find_by_password_reset_token with it:
#set_user_by_token, from ActiveRecord::Tokens
def set_user_by_token
@user = User.find_by_password_reset_token!(params[:token])
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
end
User.find_by_password_reset_token!(...) does the work here. It:
- verifies the token signature using the HMAC digest.
- decodes the payload.
- checks that the exp (expiration time) hasn’t passed.
- confirms the token’s purpose matches :password_reset.
- extracts the user ID and loads the matching user record.
All of this happens transparently via the ActiveRecord::Tokens module included in Rails 8’s ActiveRecord::Base.
⏱ Changing the Expiry
Tokens have a default 15minute expiry hardcoded within the ActiveRecord::Tokens module, usually a good balance between usability and security. But should you wish to override this just explicity declare has_token in the User model:
has_token :password_reset, expires_in: 30.minutes
🔑 In detail - How exactly does user.password_reset_token work?:
It's not a statically defined method, so it hits Rails' ActiveRecord::TokenFor::InstanceMethods' #method_missing:
# Inside ActiveRecord::TokenFor;
# activerecord/lib/active_record/token_for.rb
def method_missing(name, *args, &block)
if name.to_s =~ /\A(.+)_token\z/
generate_token_for($1.to_sym)
else
super
end
end
def generate_token_for(purpose)
self.class.token_definitions.fetch(purpose).generate_token(self)
end
The regex matches "password_reset_token", extracts :password_reset, and passes it to #generate_token_for. Rails has an internal TokenDefinition class containing a hash of supported token types and their configuration, #generate_token_for looks up the :password_reset definition, then calls #generate_token on this configuration, which then builds a signed token (actually uses ActiveSupport::MessageVerifier.generate for the cryptogrtaphy), encoding a payload (user ID, expiry, purpose) along with an HMAC digest.
The first time this is called, Rails dynamically defines a method for password_reset_token so that future calls don’t hit method_missing.
🔑 In detail - How exactly does User.find_by_password_reset_token(token) work?:
Again not a predefined method, #find_by_password_reset_token(token) also relies on Rails’ ActiveRecord::TokenFor, specifically the class level #method_missing from TokenFor::ClassMethods:
# Inside ActiveRecord::TokenFor;
# activerecord/lib/active_record/token_for.rb
def method_missing(name, *args, &block)
if name.to_s =~ /\Afind_by_(.+)_token\z/
find_by_token($1.to_sym, *args)
else
super
end
end
def find_by_token(purpose, encoded_token)
token_def = token_definitions.fetch(purpose)
id = token_def.fetch_id(encoded_token)
return nil unless id
find_by(id: id)
end
When User.find_by_password_reset_token(token) is called, the regex in #method_missing matches the method name, extracting :password_reset, which it passes to #find_by_token along with the encoded token. Then, just like in #generate_token_for, the token configuration is looked up from Rails’ TokenDefinitions, and #fetch_id(encoded_token) is called on the :password_reset definition. This method verifies and decodes the token, again via ActiveSupport::MessageVerifier, and if valid, returns the user_id that was embedded in the token.
As with the instance level #method_missing, Rails caches the dynamically generated class method after the first call to avoid repeated #method_missing hits.
Conclusion
If you rolled your own, password reset functionality can be easy to get wrong and make insecure, Rails 8's authentication code provides production ready auth with password reset straight out of the box. The source code is pretty easily readable to should you wish. But hopefully this post provides you with a good enough summary understanding of how it works so to implement your own robust Rails 8 auth functionality.
SOCIAL SHARE CARD GENERATOR