If you're running any kind of service that uses e-mail as a communication method (which is just about everyone) and you want your users to be able to take some kind of action from the email (as just about everyone does) then you should be using Signed Idempotent Action Links. Now I know what you're thinking, "Signed Idempotent Action Links? But EVERYONE knows what those are!". I know, but here's a refresher anyway (ok so I made up the term, but it's descriptive!).
They are links that perform an action (such as "Delete this comment" or "Add this to my favorites") with an included signature (that associates the URL to a specific user and verifies parameters) and are idempotent (meaning that accessing them multiple times will end in the same result). In a nutshell, they are URLs that you can click through from an email and they perform a desired action:
- whether or not the user is signed in
- without any additional button presses or clickthroughs
So now that we've gone over what we're dealing with, why would you want to use them? Well, because not everyone is logged into your service when they're checking their email. In fact, if they're checking it from a smartphone or a public computer they most likely aren't logged into your service unless you're Facebook. It is the friendliest way to allow your users to perform simple actions through email.
Calm Down, Security People
Of course the reason not to use SIAL is that if a link can perform an action without requiring a login then, well, anyone can perform that action if they have the link. Very true! However, this problem is not enough to completely bar the use of SIAL because:
- These links are being sent to people's email accounts. If your email account has been compromised, you're already in way more trouble than SIAL can give you.
- Developers can counter this issue by making any SIAL action reversible. Have a "Delete" link? Make sure you have an "Undelete" function in your app somewhere.
- Convenience trumps security for many applications. Sure, don't use SIAL to initiate wire transfers or for anything that costs money, but most applications have plenty of non-world-ending actions that can benefit from instant access.
How to Use SIAL
There are two important things to consider when using SIAL:
- You MUST be able to verify any actionable content in the URL.
- You SHOULD only allow the single action via the SIAL URL. Do not log the user in from a SIAL action.
So, how do we implement something like this? Well, it's really quite simple. Here's a method similar how it was implemented for Qup.tv. First, we create the means to sign an action in a User
model:
require 'digest/sha1' class User # ... def sign_action(action, *params) Digest::SHA1.hexdigest( "--signed--#{id}-#{action}-#{params.join('-')}-#{secret_token}" ) end def verify(signature, action, *params) signature == sign_action(action, *params) end end
What we're doing here is creating a SHA1 hash of a string that is built using a known formula and includes all of the elements needed for the action:
- id is the id of the user
- action is the name of the action that we're taking. For Qup the action might be
queue
,watch
, orview
. - params are any additional parameters that alter the outcome of the action. Again, for Qup this could be the id of the title to queue, watch, or view.
- secret_token is a unique token for the user that is not shared publicly anywhere. You can generate this using SecureRandom or find another way to implement a secret token. This should not be something like a user's password hash as it should not be determinable from any info a user would know.
So now that we have these methods for our user, how do we go about creating the actual URLs that we'll be using? Well, if we have a simple Sinatra application we can do it like so:
helpers do def authenticate_action!(signature, user_id, action, *params) @current_user = User.find(user_id) unless current_user.verify(signature, action, *params) halt 401, erb(:unauthorized) end end def action_path(user, action, *params) "/users/#{user.id}/#{action}/#{user.sign_action(action, *params)}/#{params.join('/')}" end end get "/users/:user_id/favorite/:signature/:item_id" do authenticate_action!(params[:signature], params[:user_id], 'favorite', params[:item_id]) @item = Item.find(params[:item_id]) current_user.favorites << @item unless current_user.favorites.include?(@item) erb :favorite_added end
As you can see, all we're really doing here is:
- Creating a helper that will display a 401 unauthorized message if the signature provided in the URL does not match the proper signature for the provided user.
- Creating a helper that will help us to generate URLs for our actions.
- Showing an example of how one such action could be built.
Notice that in this example I am making no use of session variables or any kind of persistent state. In fact, you should make sure that you ignore all such variables. If another user is signed in at the moment, the link should still work for the signed user.
One other thing to notice is that the item is only added to favorites if it isn't already there. This gives the action idempotence: whether you run it once or 100 times the result is the same, making sure that the item is in the user's favorites.
SIAL is not a technique that you will use in every instance, but the benefits for the user can be big in terms of convenience, and it's often the small conveniences that make a big difference when developing software that people love.
If you liked this post (or didn't) and you use Netflix Instant, go check out Qup and get email alerts (with Signed Idempotent Action Links) when new titles are added.