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
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
percentage chance of the adversary cracking the password
# File lib/omniauth/strategies/onetime.rb, line 88 def self.adversary_chance 100 / adversary_ratio end
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
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
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
# 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
# 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
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
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
# 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
# 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
# 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
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
# 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, 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