Skip to content

Instantly share code, notes, and snippets.

@brandondees
Created September 17, 2015 20:06
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brandondees/fec42cb0af6806c8c916 to your computer and use it in GitHub Desktop.
Save brandondees/fec42cb0af6806c8c916 to your computer and use it in GitHub Desktop.
Stronger Authentication Tokens
# == Schema Information
#
# Table name: authentication_tokens
#
# created_at :datetime
# expires_at :datetime
# hashed_token :string(255)
# id :integer not null, primary key
# ip_address :string(255)
# updated_at :datetime
# user_agent :string(255)
#
# Indexes
#
# index_authentication_tokens_on_hashed_token (hashed_token)
#
require 'bcrypt' unless defined? BCrypt
class AuthenticationToken < ActiveRecord::Base
has_one :user
attr_accessor :client_token
after_initialize :adjust_bcrypt_cost_for_environment
after_initialize :generate_token
def self.verified(token)
return false if token.nil? or token.blank?
parts = token.to_s.split(':')
return false if parts.length < 3
uuid = parts.first
salt = parts.second
secret = parts.last
return false unless valid_salt?(salt)
return false unless (uuid and salt and secret)
hash = BCrypt::Engine.hash_secret(peppered(secret), salt)
verified_token = self.find_by_hashed_token(token_format([uuid, salt, hash]))
return false unless verified_token
if verified_token.expired?
# log the access attempt
return false
else
verified_token.set_expiration_date and verified_token.save
end
return verified_token
end
def expired?
not self.expires_at.future?
end
def generate_token
unless persisted?
uuid = SecureRandom.uuid.gsub('-','').to_s
secret = SecureRandom.hex(64).to_s
hash = BCrypt::Password.create(peppered(secret), cost: @cost)
self.client_token = token_format [uuid, hash.salt, secret] # give to client
self.hashed_token = token_format [uuid, hash.salt, hash] # store hashed
set_expiration_date
end
end
def set_expiration_date
self.expires_at = 10.hours.from_now
end
################################################################
################################################################
private
def self.valid_salt?(salt)
!!(salt =~ /^\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}$/)
end
def persisted?
self.id ? true : false
end
def self.token_format(parts = [])
parts.join(':')
end
def token_format(parts)
self.class.token_format(parts)
end
def self.peppered(secret)
"#{secret}#{Devise.secret_key}"
end
def peppered(secret)
self.class.peppered(secret)
end
def adjust_bcrypt_cost_for_environment
# a value 10 or higher is needed for production security, but we want fast tests
@cost ||= ( Rails.env.test? ? 1 : 11 )
end
end
@brandondees
Copy link
Author

I should say this solution has been used in a project or two, but has not undergone rigorous review. Any suggestions for improvement would be nice to have. I am not sure whether this type of approach should be recommended over an existing popular/supported gem.

@brandondees
Copy link
Author

Also I'm aware we are prone to overkill, but I see no benefit to cutting it close on a moving target.

@rietta
Copy link

rietta commented Oct 27, 2016

Logging the IP address may not be appropriate in all situations. For example, it is considered PII in Europe and thus storing it in plaintext has risk. But Devise is doing that already in its default configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment