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 in the login form makes a GET request to PasswordController #new
, new template contains an email input form with an 'email password reset instructions' button.
➡️  new form submission makes a POST request 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 containing a link, a path segment of which contains an encrypted signed token embedded with the user id generated by the @user.password_reset_token
call in the reset mailer.
➡️  user clicks the email link containing the token, which makes a GET request to PasswordsController #edit
. Edit template contains a form with new password and password_confirmation fields, which also submits with the token from params.
➡️  submission of the edit form, makes a PUT request to PasswordController #update
which calls #set_user_from_token
(courtesy of a before_action
) which decrypts & validates the token, then sets @user
, then updates that User model instance with the new password. Password update is then completed, user is routed back to the new_session_path for login. Done.
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 in 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)
, a method within the ActiveRecord::Tokens module which from Rails 8 is included in ActiveRecord::Base. This module enables generation of signed, expirable, purpose-scoped tokens, as well as secure verification and decoding of those tokens (interally using 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. Exactly how these methods work is covered in detail in the final two sections of this article.
The link 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 the /:token/ path segment.
đź§© What does the actual token look like?:
The actual token generated a two part string consisting of; --, e.g.;
eyJfcmFpbHMiOnsiZGF0YSI6WzYsIlNJdDV5R...==--4d759ab766988553b56740f7ad1386d05a1e5...
The payload will be a Base64 encoded representation of a JSON hash 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
}
}
HMAC stands for Hash-based Message Authentication Code, 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 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 explicitly 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 #method_missing
regex matches "password_reset_token", extracts :password_reset from the string 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 in that class, then calls #generate_token on the token type's configuration. This then builds a token (#generate_token
uses ActiveSupport::MessageVerifier.generate for the actual cryptogrtaphy) encoding a payload (user ID, expiry, purpose) along with a signed 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 the class #method_missing
matches the method name, again regex 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 that :password_reset definition. This method verifies and decodes the token, again using ActiveSupport::MessageVerifier, and if valid returns the user_id that was embedded within 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 from scratch, password reset functionality could be easy to get wrong and make insecure. Rails 8's authentication code provides production ready auth with password reset functionality straight out of the box. The source code is pretty easily readable too should you wish. But hopefully this post has now provided you with a good enough summary understanding to complete your own implementation of this robust new Rails 8 auth functionality.
Top comments (1)
Here's the previous article which covers implementation of sign up and session auth, if you're not yet familiar with how Rails 8 auth works it would be helpful to read it before the one on this current page (even if you're not running a React front end you should still find it useful as a primer).