diff --git a/lib/one_time_password/base.rb b/lib/one_time_password/base.rb index a8c63388b6..5a71c1ed83 100644 --- a/lib/one_time_password/base.rb +++ b/lib/one_time_password/base.rb @@ -12,6 +12,10 @@ def rotp private - attr_reader :issuer + attr_reader :issuer, :secret + + def encode_secret(secret) + ROTP::Base32.encode(secret) if secret + end end end diff --git a/lib/one_time_password/generator.rb b/lib/one_time_password/generator.rb index 9ba8130650..b73b647551 100644 --- a/lib/one_time_password/generator.rb +++ b/lib/one_time_password/generator.rb @@ -1,11 +1,12 @@ module OneTimePassword class Generator < Base - def initialize(issuer = nil) + def initialize(issuer = nil, secret: nil) @issuer = issuer || ISSUER + @secret = encode_secret(secret) || SECRET end def code - @code ||= rotp.new(SECRET, issuer: issuer).now + @code ||= rotp.new(secret, issuer: issuer).now end end end diff --git a/lib/one_time_password/validator.rb b/lib/one_time_password/validator.rb index ddb1a1a7b8..9b286aa27d 100644 --- a/lib/one_time_password/validator.rb +++ b/lib/one_time_password/validator.rb @@ -1,19 +1,20 @@ module OneTimePassword class Validator < Base - def initialize(code, generated_at = nil) + def initialize(code, generated_at = nil, secret: nil) @code = code @generated_at = generated_at + @secret = encode_secret(secret) || SECRET end def valid? - code.present? && !wrong_length? && (generated_at && !expired?) && !incorrect? + code.present? && !wrong_length? && !expired? && !incorrect? end def warning return "Enter a passcode" if code.blank? return "Enter a valid passcode containing #{LENGTH} digits" if wrong_length? - return "Your passcode has expired, request a new one" if generated_at && expired? - return "Your passcode is not valid or has expired" if !generated_at + return "Your passcode has expired, request a new one" if expired? + return "Your passcode is not valid or has expired" if !generated_at && incorrect? "Enter a valid passcode" if incorrect? end @@ -30,13 +31,13 @@ def wrong_length? def expired? return @expired if defined?(@expired) - @expired = generated_at < DRIFT.seconds.ago + @expired = generated_at < DRIFT.seconds.ago if generated_at end def incorrect? return @incorrect if defined? @incorrect - @incorrect = rotp.new(SECRET, issuer: ISSUER).verify(code, drift_behind: DRIFT).nil? + @incorrect = rotp.new(secret, issuer: ISSUER).verify(code, drift_behind: DRIFT).nil? end end end diff --git a/spec/lib/one_time_password/generator_spec.rb b/spec/lib/one_time_password/generator_spec.rb index edcc186b96..10fd60f5f1 100644 --- a/spec/lib/one_time_password/generator_spec.rb +++ b/spec/lib/one_time_password/generator_spec.rb @@ -1,14 +1,29 @@ require "rails_helper" RSpec.describe OneTimePassword::Generator do + let(:totp) { instance_double ROTP::TOTP, now: one_time_passcode } + let(:one_time_passcode) { 123456 } + + before do + allow(ROTP::TOTP).to receive(:new).and_return(totp) + end + describe "#code" do subject { described_class.new.code } - let(:totp) { instance_double ROTP::TOTP, now: one_time_passcode } - let(:one_time_passcode) { 123456 } + it "generates a new code" do + expect(subject).to eq one_time_passcode + end + end + + context "specifying a secret" do + subject { described_class.new(secret: secret).code } + + let(:secret) { "somesecret" } - before do - allow(ROTP::TOTP).to receive(:new).and_return(totp) + it "uses the secret" do + expect(ROTP::TOTP).to receive(:new).with(ROTP::Base32.encode(secret), anything).and_return(totp) + subject end it "generates a new code" do diff --git a/spec/lib/one_time_password/validator_spec.rb b/spec/lib/one_time_password/validator_spec.rb index b3df1615b0..f3335c9fcb 100644 --- a/spec/lib/one_time_password/validator_spec.rb +++ b/spec/lib/one_time_password/validator_spec.rb @@ -87,4 +87,31 @@ end end end + + context "not specifying generated_at" do + subject { described_class.new(one_time_passcode) } + + context "with a valid code" do + it { is_expected.to be_valid } + + it "has no warning" do + expect(subject.warning).to be_nil + end + end + end + + context "specifying a secret" do + let!(:one_time_passcode) { OneTimePassword::Generator.new(secret: secret).code } + subject { described_class.new(one_time_passcode, secret: secret) } + + let(:secret) { "somesecretstring" } + + context "with a valid code" do + it { is_expected.to be_valid } + + it "has no warning" do + expect(subject.warning).to be_nil + end + end + end end