mirror of
https://github.com/mastodon/mastodon.git
synced 2024-10-21 17:16:09 +00:00
375 lines
11 KiB
Ruby
375 lines
11 KiB
Ruby
|
# 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
|