class OmniAuth::Strategies::Onetime

An omniauth strategy using secure onetime passwords

Constants

AdversaryMultiDevice
AdversarySingleDevice

these options are a means of modeling a theoretical adversary and ensuring some minimum level of security against that adversary the default is roughly a cluster of 100 GPU's, this is not inexpensive keep this in mind: xkcd.com/538/ cost = bcrypt cost speed = hashes per second per device at cost devices = number of devices

Public Class Methods

adversary_adjust() click to toggle source

factor to adjust bcrypt costs

# File lib/omniauth/strategies/onetime.rb, line 69
def self.adversary_adjust
  2**(default_options[:adversary][:cost] -
      default_options[:password_cost])
end
adversary_chance() click to toggle source

percentage chance of the adversary cracking the password

# File lib/omniauth/strategies/onetime.rb, line 88
def self.adversary_chance
  100 / adversary_ratio
end
adversary_ratio() click to toggle source

ratio of hashes per second needed to brute-force to the theoretical adversary, <= 1 means the adversary can crack within the time alloted higher is more secure, chance of cracking = 1 in adversary_ratio

# File lib/omniauth/strategies/onetime.rb, line 83
def self.adversary_ratio
  Rational(difficulty, adversary_speed)
end
adversary_speed() click to toggle source

hashes per second (total) at password_cost

# File lib/omniauth/strategies/onetime.rb, line 75
def self.adversary_speed
  default_options[:adversary][:speed] *
    default_options[:adversary][:devices] * adversary_adjust
end
difficulty() click to toggle source

hashes per second needed for 100% complete brute force higher is more secure

# File lib/omniauth/strategies/onetime.rb, line 63
def self.difficulty
  (26**default_options[:password_length]) /
    default_options[:password_time]
end
new(app, *args, &block) click to toggle source
Calls superclass method
# File lib/omniauth/strategies/onetime.rb, line 49
def initialize(app, *args, &block)
  super

  if options[:password_cache].nil? && defined?(Rails)
    options[:password_cache] = Rails.cache
  end

  if options[:password_cache].nil?
    raise 'omniauth-onetime must be configured with a password cache.'
  end
end

Private Instance Methods

callback_phase() click to toggle source
Calls superclass method
# File lib/omniauth/strategies/onetime.rb, line 199
def callback_phase
  log :debug, 'STEP 3: verify password'
  email = request.params['email']
  plaintext = request.params['password']

  if verify_password(email, plaintext)
    # expire password
    options[:password_cache].delete(email)
    super
  else
    fail!(:invalid_credentials)
  end
end
new_password() click to toggle source

generate password of options length of uppercase letters A-Z

# File lib/omniauth/strategies/onetime.rb, line 104
def new_password
  Array.new(options[:password_length]) do
    SecureRandom.random_number(26) + 65
  end.pack('c*')
end
prepare_password(email) click to toggle source

if a password does not exist for the email, generate one, save it to the cache, then email it to the email address provided ie generate, save, send

# File lib/omniauth/strategies/onetime.rb, line 156
def prepare_password(email)
  # to prevent DOS do not send another password until previous one has
  # expired
  unless options[:password_cache].exist?(email)
    plaintext = new_password
    save_password(email, plaintext)
    send_password(email, plaintext)
  end
end
request_email() click to toggle source
# File lib/omniauth/strategies/onetime.rb, line 166
def request_email
  log :debug, 'STEP 1: Ask user for email'

  form = OmniAuth::Form.new(title: 'User Info', url: request_path)
  form.text_field :email, :email
  form.button 'Request Password'
  form.to_response
end
request_password(email) click to toggle source
# File lib/omniauth/strategies/onetime.rb, line 175
def request_password(email)
  log :debug, 'STEP 2: prepare password then ask user for password'
  prepare_password(email)

  form = OmniAuth::Form.new(title: 'User Info', url: callback_path)
  form.text_field :password, :password
  form.html("<input type=\"hidden\" name=\"email\" value=\"#{email}\">")
  form.button 'Sign In'
  form.to_response
end
request_phase() click to toggle source
# File lib/omniauth/strategies/onetime.rb, line 186
def request_phase
  email = request.params['email']
  plaintext = request.params['password']

  if email.blank?
    request_email
  elsif plaintext.blank?
    request_password(email)
  else
    fail!(:took_a_wrong_turn)
  end
end
save_password(email, plaintext) click to toggle source

create cryoted oassword from plaintext using Bcrypt and save it to the password cache

# File lib/omniauth/strategies/onetime.rb, line 112
def save_password(email, plaintext)
  crypted = BCrypt::Password
            .create(plaintext, cost: options[:password_cost])
  options[:password_cache]
    .write(email, crypted, expires_in: options[:password_time])
end
send_password(email, plaintext) click to toggle source
# File lib/omniauth/strategies/onetime.rb, line 142
def send_password(email, plaintext)
  # break the password into groups of 4 letters for readability and
  # usability
  pw = plaintext.scan(/.{4}/).join(' ')
  link = "#{callback_url}?email=#{email}&password=#{plaintext}"
  body = "Enter this code: #{pw}\nOr click this link:\n#{link}"
  ActionMailer::Base
    .mail(options[:email_options].merge(to: email, body: body))
    .deliver_now
end
verify_password(email, plaintext) click to toggle source

verify password, case is insensitive and all spaces and characters other than A-Z are stripped out

# File lib/omniauth/strategies/onetime.rb, line 121
def verify_password(email, plaintext)
  crypted = options[:password_cache].read(email)

  begin
    (BCrypt::Password.new(crypted) == plaintext.upcase.gsub(/\W/, ''))
  rescue BCrypt::Errors::InvalidHash
    false
  end
end