about summary refs log tree commit diff
path: root/spec/controllers/auth/sessions_controller_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/controllers/auth/sessions_controller_spec.rb')
-rw-r--r--spec/controllers/auth/sessions_controller_spec.rb191
1 files changed, 136 insertions, 55 deletions
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index c387842cd..8ad9e74fc 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require 'rails_helper'
+require 'webauthn/fake_client'
 
 RSpec.describe Auth::SessionsController, type: :controller do
   render_views
@@ -183,90 +184,170 @@ RSpec.describe Auth::SessionsController, type: :controller do
     end
 
     context 'using two-factor authentication' do
-      let!(:user) do
-        Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
-      end
-
-      let!(:recovery_codes) do
-        codes = user.generate_otp_backup_codes!
-        user.save
-        return codes
-      end
-
-      context 'using email and password' do
-        before do
-          post :create, params: { user: { email: user.email, password: user.password } }
+      context 'with OTP enabled as second factor' do
+        let!(:user) do
+          Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
         end
 
-        it 'renders two factor authentication page' do
-          expect(controller).to render_template("two_factor")
+        let!(:recovery_codes) do
+          codes = user.generate_otp_backup_codes!
+          user.save
+          return codes
         end
-      end
 
-      context 'using upcase email and password' do
-        before do
-          post :create, params: { user: { email: user.email.upcase, password: user.password } }
-        end
+        context 'using email and password' do
+          before do
+            post :create, params: { user: { email: user.email, password: user.password } }
+          end
 
-        it 'renders two factor authentication page' do
-          expect(controller).to render_template("two_factor")
+          it 'renders two factor authentication page' do
+            expect(controller).to render_template("two_factor")
+            expect(controller).to render_template(partial: "_otp_authentication_form")
+          end
         end
-      end
 
-      context 'using a valid OTP' do
-        before do
-          post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id }
-        end
+        context 'using upcase email and password' do
+          before do
+            post :create, params: { user: { email: user.email.upcase, password: user.password } }
+          end
 
-        it 'redirects to home' do
-          expect(response).to redirect_to(root_path)
+          it 'renders two factor authentication page' do
+            expect(controller).to render_template("two_factor")
+            expect(controller).to render_template(partial: "_otp_authentication_form")
+          end
         end
 
-        it 'logs the user in' do
-          expect(controller.current_user).to eq user
+        context 'using a valid OTP' do
+          before do
+            post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id }
+          end
+
+          it 'redirects to home' do
+            expect(response).to redirect_to(root_path)
+          end
+
+          it 'logs the user in' do
+            expect(controller.current_user).to eq user
+          end
         end
-      end
 
-      context 'when the server has an decryption error' do
-        before do
-          allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
-          post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id }
+        context 'when the server has an decryption error' do
+          before do
+            allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
+            post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id }
+          end
+
+          it 'shows a login error' do
+            expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
+          end
+
+          it "doesn't log the user in" do
+            expect(controller.current_user).to be_nil
+          end
         end
 
-        it 'shows a login error' do
-          expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
+        context 'using a valid recovery code' do
+          before do
+            post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id }
+          end
+
+          it 'redirects to home' do
+            expect(response).to redirect_to(root_path)
+          end
+
+          it 'logs the user in' do
+            expect(controller.current_user).to eq user
+          end
         end
 
-        it "doesn't log the user in" do
-          expect(controller.current_user).to be_nil
+        context 'using an invalid OTP' do
+          before do
+            post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id }
+          end
+
+          it 'shows a login error' do
+            expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
+          end
+
+          it "doesn't log the user in" do
+            expect(controller.current_user).to be_nil
+          end
         end
       end
 
-      context 'using a valid recovery code' do
-        before do
-          post :create, params: { user: { otp_attempt: recovery_codes.first } }, session: { attempt_user_id: user.id }
+      context 'with WebAuthn and OTP enabled as second factor' do
+        let!(:user) do
+          Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
         end
 
-        it 'redirects to home' do
-          expect(response).to redirect_to(root_path)
+        let!(:recovery_codes) do
+          codes = user.generate_otp_backup_codes!
+          user.save
+          return codes
         end
 
-        it 'logs the user in' do
-          expect(controller.current_user).to eq user
+        let!(:webauthn_credential) do
+          user.update(webauthn_id: WebAuthn.generate_user_id)
+          public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
+          user.webauthn_credentials.create(
+            nickname: 'SecurityKeyNickname',
+            external_id: public_key_credential.id,
+            public_key: public_key_credential.public_key,
+            sign_count: '1000'
+           )
+          user.webauthn_credentials.take
         end
-      end
 
-      context 'using an invalid OTP' do
-        before do
-          post :create, params: { user: { otp_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id }
+        let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
+
+        let(:fake_client) { WebAuthn::FakeClient.new(domain) }
+
+        let(:challenge) { WebAuthn::Credential.options_for_get.challenge }
+
+        let(:sign_count) { 1234 }
+
+        let(:fake_credential) { fake_client.get(challenge: challenge, sign_count: sign_count) }
+
+        context 'using email and password' do
+          before do
+            post :create, params: { user: { email: user.email, password: user.password } }
+          end
+
+          it 'renders webauthn authentication page' do
+            expect(controller).to render_template("two_factor")
+            expect(controller).to render_template(partial: "_webauthn_form")
+          end
         end
 
-        it 'shows a login error' do
-          expect(flash[:alert]).to match I18n.t('users.invalid_otp_token')
+        context 'using upcase email and password' do
+          before do
+            post :create, params: { user: { email: user.email.upcase, password: user.password } }
+          end
+
+          it 'renders webauthn authentication page' do
+            expect(controller).to render_template("two_factor")
+            expect(controller).to render_template(partial: "_webauthn_form")
+          end
         end
 
-        it "doesn't log the user in" do
-          expect(controller.current_user).to be_nil
+        context 'using a valid webauthn credential' do
+          before do
+            @controller.session[:webauthn_challenge] = challenge
+
+            post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id }
+          end
+
+          it 'instructs the browser to redirect to home' do
+            expect(body_as_json[:redirect_path]).to eq(root_path)
+          end
+
+          it 'logs the user in' do
+            expect(controller.current_user).to eq user
+          end
+
+          it 'updates the sign count' do
+            expect(webauthn_credential.reload.sign_count).to eq(sign_count)
+          end
         end
       end
     end