# frozen_string_literal # # Model for managing one-time password credentials for a given Member # # Automatically generated by the orthodox gem (https://github.com/katanacode/orthodox) # (c) Copyright 2019 Katana Code Ltd. All Rights Reserved. # # == Schema Information # # Table name: otp_credentials # # id :bigint not null, primary key # authable_type :string not null # last_used_at :datetime # recovery_codes :json # secret :string(32) # authable_id :bigint not null # # Indexes # # index_otp_credentials_on_authable_type_and_authable_id (authable_type,authable_id) # class OtpCredential < ApplicationRecord ## # How much of a 'grace' period should we give the user, after which we will accept # expired OTPs. Time in seconds. DRIFT_ALLOWANCE = 15 # ============== # = Attributes = # ============== attr_readonly :authable_type, :authable_id serialize :recovery_codes # ================ # = Associations = # ================ belongs_to :authable, polymorphic: true # ============= # = Callbacks = # ============= before_create :set_secret before_create :set_last_used_at before_create :set_recovery_codes # =========================== # = Public instance methods = # =========================== # URL for generating QR code for this OTP. # # Returns String def url totp.provisioning_uri(authable.email) end # Test the given code against the expected current value. # # Returns Integer (Timestamp) # Returns nil def valid_otp?(test_value) if result = totp.verify(test_value, after: last_used_at, drift_behind: DRIFT_ALLOWANCE) touch(:last_used_at) end result end # Test the given recovery code against the stored recovery_codes # # Returns Boolean def valid_recovery_code?(test_value) test_value.to_s.in?(recovery_codes) end # Removes a used recovery code from the recovery_codes list. # # Returns Boolean def consume_recovery_code!(recovery_code) array = recovery_codes array.delete(recovery_code) update_attribute(:recovery_codes, array) end private # The expected current OTP value. This shouldn't need to be required in production # # Returns String def current_otp totp.now end # Set the secret value to a random base 32 String # # Returns String def set_secret self.secret = ROTP::Base32.random end # Ensure the last_used_at time is always present and a past datetime. def set_last_used_at self.last_used_at = 5.minutes.ago end def set_recovery_codes self.recovery_codes = 10.times.map { generate_recovery_code } end def generate_recovery_code "#{SecureRandom.hex(3)[0..4]}-#{SecureRandom.hex(3)[0..4]}" end # An instance of the TOTP to test codes against. # # Returns ROTP::TOTP def totp @totp ||= ROTP::TOTP.new(secret, issuer: "<%= Rails.application.class.module_parent.name %>") end end