MortalToken, because some tokens shouldn’t live forever

MortalToken is a convenience wrapper for HMAC-based authentication. The tokens self-destruct after a specified time period; no need to store and look them up for verification.

The default lifespan is one hour. For a Web-based application, you might extend this to many hours, or you might cut it back to only a few minutes, issuing a new token for each request/response cycle.

My original use case was login auth tokens for some simple Sinatra apps. I can also see it being useful for API auth. Of course there are potential uses outside of HTTP, too.

Steps:

  1. Generate a new token

  2. Give the client the resulting digest, salt, and expiry timestamp

  3. Time passes

  4. Receive a digest, salt, and expiry timestamp from client

  5. Reconstitute the token from salt and timestamp, then see if the digests match. If not, the token has expired (or was forged).

Warning

It is up to you to transmit the digest, salt, and expiry timestamp securely. If it’s intercepted, someone could impersonate your client until the token expires. In other words, this is only part of an authentication solution. Use with caution. May contain traces of peanut.

Install

[sudo] gem install mortal-token

Or add it to your Gemfile

gem "mortal-token"

Example use with Sinatra

require 'sinatra'
require 'mortal-token'

MortalToken.secret = 'asdf092$78roasdjfjfaklmsdadASDFopijf98%2ejA#Df@sdf'

post '/login' do
  if 
    token = MortalToken.new
    # Or "token = MortalToken.new(current_user.id)" to set your own salt and verify who owns the session.
    session[:salt] = token.salt
    session[:expires] = token.expires
    session[:digest] = token.digest
    redirect '/secret'
  end
end

get '/secret' do
  if MortalToken.check(session[:salt], session[:expires]).against(session[:digest])
    'Nice token!'
  else
    'Your token is expired or forged!'
  end
end

Automatically re-issue nearly expired tokens

MortalToken.check(session[:salt], session[:expires]).against(session[:digest]) do |token|
  session[:salt], session[:expires], session[:digest] = MortalToken.new.get if token.expires_soon?
end

Checking token validity explained

MortalToken.check(salt, expires).against(digest)

is syntactic sugar for

reconstituted_token = MortalToken.new(salt, expires)
reconstituted_token == digest

A token’s == and === methods accept another token or a digest. In the example above, an attempt has been made to reconstitute the original token using it’s salt and expiry timestamp. To be considered “equal”, the reconstituted token’s digest must match the original digest AND the timestamp must be in the future. Unless both of those conditions are met, then token is considered invalid or expired.

Tweak token parameters

You may tweak certain parameters of the library in order to make it more secure, less, faster, etc. These are the defaults (see the MortalToken class for documentation about each parameter):

MortalToken.valid_for = 1         # tokens are valid for N units
MortalToken.units = :hours        # or :days, :minutes
MortalToken.digest = 'sha256'     # The digest algorithm used by HMAC
MortalToken.max_salt_length = 50  # Maximum token salt length
MortalToken.min_salt_length = 10  # Minimum token salt length

Multiple configurations

Your application may want to use MortalTokens in various contexts, where the same parameters may not make sense (probably units and valid_for). You may define different scopes and give them each their own config. Always define the default scope first (above). Other scopes will inherit its secret unless you specify another.

MortalToken.config(:foo) do |config|
  config.units = :minutes
  config.valid_for = 10
end

token = MortalToken.use(:foo).token

License

Copyright 2012 Jordan Hollinger

Licensed under the Apache License