Created
September 17, 2015 20:06
-
-
Save brandondees/fec42cb0af6806c8c916 to your computer and use it in GitHub Desktop.
Stronger Authentication Tokens
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# == 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 |
Also I'm aware we are prone to overkill, but I see no benefit to cutting it close on a moving target.
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
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.