diff options
Diffstat (limited to 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb')
-rw-r--r-- | spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb new file mode 100644 index 000000000..fe53b4dfc --- /dev/null +++ b/spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb @@ -0,0 +1,374 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'webauthn/fake_client' + +describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do + render_views + + let(:user) { Fabricate(:user) } + let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" } + let(:fake_client) { WebAuthn::FakeClient.new(domain) } + + def add_webauthn_credential(user) + Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key') + end + + describe 'GET #new' do + context 'when signed in' do + before do + sign_in user, scope: :user + end + + context 'when user has otp enabled' do + before do + user.update(otp_required_for_login: true) + end + + it 'returns http success' do + get :new + + expect(response).to have_http_status(200) + end + end + + context 'when user does not have otp enabled' do + before do + user.update(otp_required_for_login: false) + end + + it 'requires otp enabled first' do + get :new + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + end + + describe 'GET #index' do + context 'when signed in' do + before do + sign_in user, scope: :user + end + + context 'when user has otp enabled' do + before do + user.update(otp_required_for_login: true) + end + + context 'when user has webauthn enabled' do + before do + user.update(webauthn_id: WebAuthn.generate_user_id) + add_webauthn_credential(user) + end + + it 'returns http success' do + get :index + + expect(response).to have_http_status(200) + end + end + + context 'when user does not has webauthn enabled' do + it 'redirects to 2FA methods list page' do + get :index + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + + context 'when user does not have otp enabled' do + before do + user.update(otp_required_for_login: false) + end + + it 'requires otp enabled first' do + get :index + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + + context 'when not signed in' do + it 'redirects to login' do + delete :index + + expect(response).to redirect_to new_user_session_path + end + end + end + + describe 'GET /options #options' do + context 'when signed in' do + before do + sign_in user, scope: :user + end + + context 'when user has otp enabled' do + before do + user.update(otp_required_for_login: true) + end + + context 'when user has webauthn enabled' do + before do + user.update(webauthn_id: WebAuthn.generate_user_id) + add_webauthn_credential(user) + end + + it 'returns http success' do + get :options + + expect(response).to have_http_status(200) + end + + it 'stores the challenge on the session' do + get :options + + expect(@controller.session[:webauthn_challenge]).to be_present + end + + it 'does not change webauthn_id' do + expect { get :options }.to_not change { user.webauthn_id } + end + + it "includes existing credentials in list of excluded credentials" do + get :options + + excluded_credentials_ids = JSON.parse(response.body)['excludeCredentials'].map { |credential| credential['id'] } + expect(excluded_credentials_ids).to match_array(user.webauthn_credentials.pluck(:external_id)) + end + end + + context 'when user does not have webauthn enabled' do + it 'returns http success' do + get :options + + expect(response).to have_http_status(200) + end + + it 'stores the challenge on the session' do + get :options + + expect(@controller.session[:webauthn_challenge]).to be_present + end + + it 'sets user webauthn_id' do + get :options + + expect(user.reload.webauthn_id).to be_present + end + end + end + + context 'when user has not enabled otp' do + before do + user.update(otp_required_for_login: false) + end + + it 'requires otp enabled first' do + get :options + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + + context 'when not signed in' do + it 'redirects to login' do + get :options + + expect(response).to redirect_to new_user_session_path + end + end + end + + describe 'POST #create' do + let(:nickname) { 'SecurityKeyNickname' } + + let(:challenge) do + WebAuthn::Credential.options_for_create( + user: { id: user.id, name: user.account.username, display_name: user.account.display_name } + ).challenge + end + + let(:new_webauthn_credential) { fake_client.create(challenge: challenge) } + + context 'when signed in' do + before do + sign_in user, scope: :user + end + + context 'when user has enabled otp' do + before do + user.update(otp_required_for_login: true) + end + + context 'when user has enabled webauthn' do + before do + user.update(webauthn_id: WebAuthn.generate_user_id) + add_webauthn_credential(user) + end + + context 'when creation succeeds' do + it 'returns http success' do + @controller.session[:webauthn_challenge] = challenge + + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + + expect(response).to have_http_status(200) + end + + it 'adds a new credential to user credentials' do + @controller.session[:webauthn_challenge] = challenge + + expect do + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + end.to change { user.webauthn_credentials.count }.by(1) + end + + it 'does not change webauthn_id' do + @controller.session[:webauthn_challenge] = challenge + + expect do + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + end.to_not change { user.webauthn_id } + end + end + + context 'when the nickname is already used' do + it 'fails' do + @controller.session[:webauthn_challenge] = challenge + + post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' } + + expect(response).to have_http_status(500) + expect(flash[:error]).to be_present + end + end + + context 'when the credential already exists' do + before do + user2 = Fabricate(:user) + public_key_credential = WebAuthn::Credential.from_create(new_webauthn_credential) + Fabricate(:webauthn_credential, + user_id: user2.id, + external_id: public_key_credential.id, + public_key: public_key_credential.public_key) + end + + it 'fails' do + @controller.session[:webauthn_challenge] = challenge + + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + + expect(response).to have_http_status(500) + expect(flash[:error]).to be_present + end + end + end + + context 'when user have not enabled webauthn' do + context 'creation succeeds' do + it 'creates a webauthn credential' do + @controller.session[:webauthn_challenge] = challenge + + expect do + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + end.to change { user.webauthn_credentials.count }.by(1) + end + end + end + end + + context 'when user has not enabled otp' do + before do + user.update(otp_required_for_login: false) + end + + it 'requires otp enabled first' do + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + + context 'when not signed in' do + it 'redirects to login' do + post :create, params: { credential: new_webauthn_credential, nickname: nickname } + + expect(response).to redirect_to new_user_session_path + end + end + end + + describe 'DELETE #destroy' do + context 'when signed in' do + before do + sign_in user, scope: :user + end + + context 'when user has otp enabled' do + before do + user.update(otp_required_for_login: true) + end + + context 'when user has webauthn enabled' do + before do + user.update(webauthn_id: WebAuthn.generate_user_id) + add_webauthn_credential(user) + end + + context 'when deletion succeeds' do + it 'redirects to 2FA methods list and shows flash success' do + delete :destroy, params: { id: user.webauthn_credentials.take.id } + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:success]).to be_present + end + + it 'deletes the credential' do + expect do + delete :destroy, params: { id: user.webauthn_credentials.take.id } + end.to change { user.webauthn_credentials.count }.by(-1) + end + end + end + + context 'when user does not have webauthn enabled' do + it 'redirects to 2FA methods list and shows flash error' do + delete :destroy, params: { id: '1' } + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + + context 'when user does not have otp enabled' do + it 'requires otp enabled first' do + delete :destroy, params: { id: '1' } + + expect(response).to redirect_to settings_two_factor_authentication_methods_path + expect(flash[:error]).to be_present + end + end + end + + context 'when not signed in' do + it 'redirects to login' do + delete :destroy, params: { id: '1' } + + expect(response).to redirect_to new_user_session_path + end + end + end +end |