diff --git a/Rakefile b/Rakefile index 7723dfbb..9a575025 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,9 @@ +begin + require 'bundler/setup' +rescue LoadError + puts 'You must `gem install bundler` and `bundle install` to run rake tasks' +end + require "bundler/gem_tasks" APP_RAKEFILE = File.expand_path("../spec/rails_app/Rakefile", __FILE__) diff --git a/lib/two_factor_authentication/controllers/helpers.rb b/lib/two_factor_authentication/controllers/helpers.rb index 64e8377c..501f4d9a 100644 --- a/lib/two_factor_authentication/controllers/helpers.rb +++ b/lib/two_factor_authentication/controllers/helpers.rb @@ -20,14 +20,15 @@ def handle_two_factor_authentication end def handle_failed_second_factor(scope) - if request.format.present? - if request.format.html? - session["#{scope}_return_to"] = request.original_fullpath if request.get? - redirect_to two_factor_authentication_path_for(scope) - elsif request.format.json? - session["#{scope}_return_to"] = root_path(format: :html) - render json: { redirect_to: two_factor_authentication_path_for(scope) }, status: :unauthorized - end + if request.format&.html? + session["#{scope}_return_to"] = request.original_fullpath if request.get? + redirect_to two_factor_authentication_path_for(scope) + elsif request.format&.json? + session["#{scope}_return_to"] = root_path(format: :html) + render json: { + redirect_to: two_factor_authentication_path_for(scope), + authentication_type: send("current_#{scope}")&.direct_otp ? :otp : :totp + }, status: :unauthorized else head :unauthorized end diff --git a/lib/two_factor_authentication/models/two_factor_authenticatable.rb b/lib/two_factor_authentication/models/two_factor_authenticatable.rb index 6d73a0fb..d23cae87 100644 --- a/lib/two_factor_authentication/models/two_factor_authenticatable.rb +++ b/lib/two_factor_authentication/models/two_factor_authenticatable.rb @@ -101,7 +101,7 @@ def generate_totp_secret def create_direct_otp(options = {}) # Create a new random OTP and store it in the database digits = options[:length] || self.class.direct_otp_length || 6 - update_attributes( + update( direct_otp: random_base10(digits), direct_otp_sent_at: Time.now.utc ) @@ -122,7 +122,7 @@ def direct_otp_expired? end def clear_direct_otp - update_attributes(direct_otp: nil, direct_otp_sent_at: nil) + update(direct_otp: nil, direct_otp_sent_at: nil) end end diff --git a/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb b/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb index 6fb4f505..6e58fd1d 100644 --- a/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +++ b/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb @@ -138,7 +138,7 @@ def instance.send_two_factor_authentication_code(code) it "returns uri with user's email" do expect(instance.provisioning_uri). - to match(%r{otpauth://totp/houdini@example.com\?secret=\w{32}}) + to match(%r{otpauth://totp/houdini%40example.com\?secret=\w{32}}) end it 'returns uri with issuer option' do diff --git a/spec/rails_app/app/assets/config/manifest.js b/spec/rails_app/app/assets/config/manifest.js new file mode 100644 index 00000000..a3d7d420 --- /dev/null +++ b/spec/rails_app/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link application.css +//= link application.js diff --git a/spec/rails_app/app/controllers/home_controller.rb b/spec/rails_app/app/controllers/home_controller.rb index f55da102..7007d886 100644 --- a/spec/rails_app/app/controllers/home_controller.rb +++ b/spec/rails_app/app/controllers/home_controller.rb @@ -5,6 +5,15 @@ def index end def dashboard + respond_to do |format| + format.html + format.json do + render json: {success: true} + end + format.xml do + render xml: "" + end + end end end diff --git a/spec/rails_app/app/models/guest_user.rb b/spec/rails_app/app/models/guest_user.rb index 8003624c..1222279a 100644 --- a/spec/rails_app/app/models/guest_user.rb +++ b/spec/rails_app/app/models/guest_user.rb @@ -7,7 +7,7 @@ class GuestUser attr_accessor :direct_otp, :direct_otp_sent_at, :otp_secret_key, :email, :second_factor_attempts_count, :totp_timestamp - def update_attributes(attrs) + def update(attrs) attrs.each do |key, value| send(key.to_s + '=', value) end diff --git a/spec/requests/api_request_spec.rb b/spec/requests/api_request_spec.rb new file mode 100644 index 00000000..c7b48ad8 --- /dev/null +++ b/spec/requests/api_request_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe "API request", type: :request do + context "when logged in" do + let(:user) { create_user("encrypted", email: "foo@example.com", otp_secret_key: "6iispf5cjufa4vsm") } + + before do + sign_in user + end + + context "with json" do + context "with totp authentication" do + it "returns 401 when path requires authentication" do + get "/dashboard.json" + expect(response.response_code).to eq(401) + body = JSON.parse(response.body) + expect(body["redirect_to"]).to eq(user_two_factor_authentication_path) + expect(body["authentication_type"]).to eq("totp") + end + end + + context "with direct otp authentication" do + it "returns 401 when path requires authentication" do + user.update!(direct_otp: true) + get "/dashboard.json" + expect(response.response_code).to eq(401) + body = JSON.parse(response.body) + expect(body["redirect_to"]).to eq(user_two_factor_authentication_path) + expect(body["authentication_type"]).to eq("otp") + end + end + + context "after TFA" do + it "returns successfully" do + get "/dashboard.json" + expect(response.response_code).to eq(401) + expect(JSON.parse(response.body)["redirect_to"]).to eq(user_two_factor_authentication_path) + totp_code = ROTP::TOTP.new(user.otp_secret_key, digits: 6).at(Time.now) + put "/users/two_factor_authentication", params: { code: totp_code } + get "/dashboard.json" + expect(response.response_code).to eq(200) + body = JSON.parse(response.body) + expect(body["success"]).to eq(true) + end + end + end + + context "with xml" do + it "returns 401 when path requires authentication" do + get "/dashboard.xml" + expect(response.response_code).to eq(401) + end + + context "after TFA" do + it "returns successfully" do + totp_code = ROTP::TOTP.new(user.otp_secret_key, digits: 6).at(Time.now) + put "/users/two_factor_authentication", params: { code: totp_code } + get "/dashboard.xml" + expect(response.response_code).to eq(200) + expect(response.body).to eq("") + end + end + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 63704333..02ac6723 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,6 +21,8 @@ config.order = 'random' config.after(:each) { Timecop.return } + + config.include Devise::Test::IntegrationHelpers, type: :request end Dir["#{Dir.pwd}/spec/support/**/*.rb"].each {|f| require f}