Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Avoiding timing based attacks with Token Authentication

By quantising the time taken for failed lookups we can mitigate timing based attacks. This allows a regular DB or cache lookup on the token to be used without revealing information about how good the candidate match is and thus thwarts timing attacks.

def authenticate_user_from_token!
  auth_token = params[Devise.token_authentication_key]
  if auth_token
    t = Time.now
    if (user = User.where(authentication_token: auth_token).first)
      sign_in user, store: false
    else
      # ensure requests with a failed token match are quantised to 200ms
      sleep((200 - Time.now + t) % 200) / 1000.0)
    end
  end
end

Yet another approach is to add a small random delay if the token is not matched.

def authenticate_user_from_token!
  auth_token = params[Devise.token_authentication_key]
  if auth_key
    if (user = User.where(authentication_token: auth_token).first)
      sign_in user, store: false
    else
      # sleep 200-400ms
      sleep(200 + rand(200)) / 1000.0)
    end
  end
end

However that said, I think I would prefer an approach similar to Rails signed and encrypted cookies. With a sever secret key, one can perform a decrypt and authenticate of the token without any database overhead and recover the token expiry AND the user id (or anything else you wish to stuff in the token). This avoids any requirement to store tokens in the database as they can be created on the fly (eg multiple devices/client instances per user). The use of AES GCM or AES CCM authenticated encryption modes would work very well.

(Untested). But something like the following for token encryption/decryption

# auth_token.rb
require 'openssl'
require 'base64'

class AuthTokenCipher
  CIPHER_MODE = 'aes-256-gcm'

  def self.make_key
     OpenSSL::Cipher.new(CIPHER_MODE).encrypt.random_key
  end

  def self.encrypt(key, data)
    cipher = OpenSSL::Cipher.new(CIPHER_MODE).encrypt
    iv = cipher.random_iv
    cipher.key = key
    cipher.auth_data = ""
    cipher.update(data)cipher.final
    Base64.strict_encode64(iv + cipher.update(data) + cipher.final + cipher.auth_tag)
  end

  # raises AurgumentError or CipherError if the token is bad
  def self.decrypt(key, data)
    data = Base64.strict_decode64 data
    cipher = OpenSSL::Cipher.new(CIPHER_MODE).decrypt
    cipher.iv = data[0..15]
    cipher.key = key
    cipher.auth_tag = data[-16..-1]
    cipher.auth_data = ""
    cipher.update(data[16..-17]) + cipher.final
  end
end

And use as follows:

def authenticate_user_from_token!
  auth_token = params[Devise.token_authentication_key]
  if auth_token
    # raises AurgumentError or CipherError if the token is bad
    auth_data = AuthTokenCipher.decrypt TOKEN_AUTH_KEY, auth_token

    # at this point we have authenticated data so we can trust the expiry and user id was what we set originally

    # check expiry and lookup the user (if needed)
    expiry, user_id = auth_data.split ':'
    if Time.now < Time.at(expiry) && (user = User.find(user_id))
      sign_in user, store: false
    end
  end
end

Creating the auth token, eg after login with username/password would be achieved as follows:

AuthTokenCipher.encrypt TOKEN_AUTH_KEY, "#{user.id}:#{(Time.now + TOKEN_TTL).to_i}"

As I said, none of the above code has been tested or used in anger and is simply provided as an idea for discussion.

The use the above cipher mode you will need a recent version of OpenSSL (>= 1.0.1) on your system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.