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.