Many web applications have the need to send links to users that allow the user to perform an action without logging in. Examples include password reset and confirming an email address. These links need to embed some kind of identifying information so the server can discern which user is performing the action. If the action is sensitive, the link should also include some amount of obfuscation so the URLs cannot be guessed or generated by nefarious actors.
We have a few ways to generate links that serve this purpose. Many applications start with something like Rails’ has_secure_token. This helper wraps up the logic to generate and persist tokens in a datastore. This approach is well understood and easy to reason about.
What are some of the downsides of storing tokens on the server? Storing the tokens in plaintext means if an attacker gets access to the application’s database, all of the stored tokens are exposed. Hashing (or digesting) the tokens asymmetrically fixes this issue but we still have to persist and protect this data.
What if we don’t need to persist the token at all? What if we could generate a token, send it out in a link, and then verify it when it gets sent back to us? In this way, we don’t have to store the tokens, which means there is less to protect.
We can achieve this stateless approach with encoded and signed tokens. Rails provides a mechanism to generate and verify tokens via ActiveSupport::MessageEncryptor
and ActiveSupport::MessageVerifier
.
We recently implemented stateless tokens in our app so users could confirm subscriptions to mailing lists. Below we’ll go through some code to get us there.
Let’s look at some code
We have a controller with a create
action that allows a user to submit an email address and a confirmations
action that verifies a submitted token.
class SubscriptionsController < ApplicationController
def create
subscription = Subscription.build(email: subscription_params[:email])
if subscription.save
SubscriptionsMailer.confirm_subscription(subscription.id).deliver_later
redirect_to subscriptions_path
else
redirect_to subscriptions_path
end
end
def confirmations
subscription = Subscription.verify_token_and_find(token: params[:token])
if subscription.present?
subscription.touch(:confirmed_at)
flash[:notice] = "Thanks for subscribing!"
else
flash[:error] = "Oh no! Your token is not valid."
end
redirect_to subscriptions_path
end
private
def subscription_params
params.require(:subscription).permit(:email)
end
end
And our model looks like this:
class Subscription < ApplicationRecord
CONFIRMATION_IN_DAYS = 7
scope :confirmed, lambda { where.not(confirmed_at: nil) }
validates :email, uniqueness: true
def generate_token(expiration_in_days: CONFIRMATION_IN_DAYS)
expiration = Time.current + expiration_in_days.days
token_values = { id: id, expiration: expiration }
self.class.encryptor.encrypt_and_sign(token_values)
end
def self.verify_token_and_find(token:)
begin
data = encryptor.decrypt_and_verify(token)
given_expiration = data[:expiration]
if given_expiration < Time.current
return nil
end
find(data[:id])
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
end
def self.encryptor
key_password_for_mailing_list = ENV.fetch('KEY_PASSWORD_FOR_SUBSCRIPTION')
SUBSCRIPTION_ENCRYPTOR_KEY = ActiveSupport::KeyGenerator
.new(key_password_for_mailing_list)
.generate_key(salt, 32)
ActiveSupport::MessageEncryptor.new(SUBSCRIPTION_ENCRYPTOR_KEY)
end
end
Our mailer is relatively straightforward except for the line where we call subscription.generate_token
, which does the heavy lifting for generating and signing our token:
class SubscriptionsMailer < ActionMailer::Base
def confirm_subscription(subscription_id)
subscription = Subscription.find(subscription_id)
@link = confirmation_url(subscription: subscription)
mail(
from: "[email protected]",
subject: "Please Confirm Your Subscription",
to: subscription.email
) do |format|
format.text
format.html
end
end
private
def confirmation_url(subscription:)
subscriptions_url(token: subscription.generate_token)
end
end
What’s the catch?
One downside of this approach is the need to manage another key, which involves protecting it (likely via using environment variables) and being able to rotate the key in an operationally simple manner. There is not a great option out of the box to manage these workflows but it’s worth considering the costs and benefits of not storing this data.
Conclusion
We wanted to find a way to avoid storing keys in our database for security and storage reasons and this approach serves our purpose. Expiring, rotating, and protecting keys still require forethought, but that’s nothing new.
Want to learn more?
- This RailsConf talk has a nice overview of thinking in stateless terms
- Check out the source code
- Read these blog posts for a more in depth overview of using these tools
- If this stuff is interesting to you and you’d like to chat, drop us a line: engineering@backerkit.com