Created
May 17, 2018 16:49
-
-
Save Nitrino/f941c7892bf755bcad56b19df18898de to your computer and use it in GitHub Desktop.
Rails mixin module for Two Factor Authentication (TOTP)
This file contains 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
module OneTimePassword | |
# Concern containing logic and methods for OTP authentication. | |
# Is used Time-based One-time Password Algorithm(TOTP) | |
# https://tools.ietf.org/html/rfc6238 | |
extend ActiveSupport::Concern | |
OTP_DIGITS = 6 | |
OTP_NUMBER_OF_BACKUP_CODES = 10 | |
OTP_BACKUP_CODE_LENGTH = 12 | |
included do | |
before_create do | |
regenerate_otp_secret | |
end | |
end | |
# Generation of a new OTP secret key | |
# After changing the OTP secret key, the provisioning uri will also change | |
def regenerate_otp_secret | |
self.otp_secret_key = ROTP::Base32.random_base32 | |
end | |
# Generation of a list backup codes | |
# 1) Invalidates all existing backup codes | |
# 2) Generates OTP_NUMBER_OF_BACKUP_CODES backup codes | |
# 3) Stores the hashed backup codes in the otp_backup_codes | |
# 4) Returns a plaintext array of the generated backup codes | |
def regenerate_otp_backup_codes | |
codes = Array.new(OTP_NUMBER_OF_BACKUP_CODES).map { SecureRandom.hex(OTP_BACKUP_CODE_LENGTH / 2) } | |
hashed_codes = codes.map { |code| Devise::Encryptor.digest(self.class, code) } | |
self.otp_backup_codes = hashed_codes | |
codes | |
end | |
# Returns true and invalidates the given code | |
# iff that code is a valid backup code. | |
def invalidate_otp_backup_code(code) | |
codes = self.otp_backup_codes || [] | |
codes.each do |backup_code| | |
next unless Devise::Encryptor.compare(self.class, backup_code, code) | |
codes.delete(backup_code) | |
self.otp_backup_codes = codes | |
self.save! | |
return true | |
end | |
false | |
end | |
def otp_is_enabled? | |
self.otp_is_enabled | |
end | |
# Authentication flow is enabled only if otp_is_enabled field is true | |
def enable_otp | |
codes = regenerate_otp_backup_codes | |
self.otp_is_enabled = true | |
self.save! | |
codes | |
end | |
def disable_otp | |
self.otp_is_enabled = false | |
self.save | |
end | |
# Shows the current OTP code. | |
# The code is similar to what will be displayed in the Google authenticator. | |
# You can use it to authenticate via SMS and for testing. | |
# @param time [Time] Time for code verification. By default is the current time. | |
# Explicit changing the test time is convenient for testing | |
def otp_code(time = Time.current) | |
ROTP::TOTP.new(self.otp_secret_key, digits: OTP_DIGITS).at(time, true) | |
end | |
# Method for verifying OTP code | |
# @param code [String] OTP code. | |
# @param drift [Integer] Allows to confirm the OTP code within 15 seconds after the expiration of its time. | |
# Drift allows you to level out the inaccuracy of time on different devices and server | |
# By default, the drift is set to 15 seconds. | |
# @return [Boolean] OTP code verification result | |
def authenticate_otp(code, drift = 15) | |
totp = ROTP::TOTP.new(self.otp_secret_key, digits: OTP_DIGITS) | |
totp.verify_with_drift(code, drift) | |
end | |
# Method for combining otp authorization and code recovery | |
def authenticate_otp_or_invalidate_otp_backup_code(code) | |
authenticate_otp(code) || invalidate_otp_backup_code(code) | |
end | |
# URI compatible with the Google Authenticator App to be scanned with the in-built QR Code scanner. | |
# @param account [String] account name for which the Provisioning URI is generated. By default user email address | |
# @param options [Hash] options for generate provisioning uri | |
# By default is blank hash. | |
def provisioning_uri(account: nil, options: {}) | |
account ||= self.attributes["email"] | |
options[:issuer] ||= otp_issuer | |
ROTP::TOTP.new(self.otp_secret_key, options).provisioning_uri(account) | |
end | |
# Exclude the OTP secret key from all serializers | |
def serializable_hash(options = nil) | |
options ||= {} | |
options[:except] = Array(options[:except]) | |
options[:except] << "otp_secret_key" | |
super(options) | |
end | |
def otp_issuer | |
raise "You must override `otp_issuer` method in #{self.class.name} model" | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment