Automatic Login Links
Scary, I know, but hear me out. Implemented correctly, an automatic login link can be just the ticket to appease those pesky, forgetful, real-world users.
Imagine a Rails application that sells fruit. The app has two kinds of users:
- A customer who comes to the site to buy fruit
- A seller who registers at the site to sell and ship fruit to customers
If you’re buying your fruit online, you’re probably pretty tech-savvy. Odds are you have accounts at a lot of sites and you do a decent job of remembering your passwords. And when memory fails, there’s the always-helpful “Forget your password?” link.
But what about the sellers? These guys are a bunch of farmers and not necessarily the most adept computer users. But every time a new order comes in, the seller gets an email prompting them to log in and ship the order.
Spoiler alert: The sellers never remember their passwords.
In this case, an automatic login link can go a long way to keep the sellers and the customers happy. However, with this approach, there are two important bases to cover in order to protect yourself from brute force attacks:
- The link must be secure (random)
- Access to the link must be temporary
In order to make the link temporary, we’ll attach each link to an order rather than to the seller. That way we can expire the link as soon as the order is shipped. Here’s the order model which automatically generates a unique login token when an order is placed and clears the login token when the order ships:
class Order < ActiveRecord::Base belongs\_to :seller belongs\_to :customer before\_create :set\_login\_token scope :authenticatable, where("orders.login\_token IS NOT NULL") def ship\_it! \# TODO Send the fruit off to the customer update\_attribute(:login\_token, nil) end private def set\_login\_token self.login\_token = ActiveSupport::SecureRandom.urlsafe\_base64 end end
Now we need a route for the automatic login link:
ApplesAndOranges::Application.routes.draw do resources :orders, :only => \[:index, :show, :destroy\] do member do get :authenticate post :ship end end end
And in the orders controller, we can automatically log the seller in when she follows the new route:
class OrdersController < ApplicationController respond\_to :html before\_filter :require\_current\_seller, :except => :authenticate before\_filter :load\_order, :except => \[:index, :authenticate\] def index `orders = current_seller.orders respond_with `orders end def show respond\_with @order end def destroy `order.destroy respond_with `order end def authenticate order = Order.authenticatable.find\_by\_login\_token!(params\[:id\]) \# TODO Force-log-in order.seller redirect\_to order end def ship `order.ship_it! respond_with `order end private def load\_order @order = current\_seller.orders.find(params\[:id\]) end end
And that’s it! Now, in the order notification email, we can send the automatic login link to the seller:
Dear <%= @seller.name %>, Congratulations! You have a new order. View your order here: <%= authenticate\_order\_url(@order.login\_token) %> Thank you!
There may be some hesitation with allowing direct authentication like this and in a many cases, it’s justified. This technique certainly doesn’t fit all applications. As always… proceed with extreme caution.
That said, this approach can be extremely useful in certain situations. If you’re already taking advantage of the “Forget your password?” functionality mentioned above, you’re probably sending similar, perishable, password-reset links to your users. Automatic login links are no less secure. Either way, an intruder has to either gain access to your email account or guess the random link.
It may be scary but I hope this helps shed some light on the technique and how it can be done securely. Your farmers will love it!
Update: Jeffrey Paul also wrote a blog post today on this very subject, explaining his own frustration with constant prompts to log in. You can read his post here.