Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- `app/controllers/accounts_controller.rb`:
  Upstream change too close to a glitch-soc change related to
  instance-local toots. Merged upstream changes.
- `app/services/fan_out_on_write_service.rb`:
  Minor conflict due to glitch-soc's handling of Direct Messages,
  merged upstream changes.
- `yarn.lock`:
  Not really a conflict, caused by glitch-soc-only dependencies
  being textually too close to updated upstream dependencies.
  Merged upstream changes.
This commit is contained in:
Thibaut Girka
2020-08-30 16:13:08 +02:00
109 changed files with 2948 additions and 1238 deletions

View File

@ -1,20 +1,51 @@
require 'rails_helper'
require 'webauthn/fake_client'
describe Admin::TwoFactorAuthenticationsController do
render_views
let(:user) { Fabricate(:user, otp_required_for_login: true) }
let(:user) { Fabricate(:user) }
before do
sign_in Fabricate(:user, admin: true), scope: :user
end
describe 'DELETE #destroy' do
it 'redirects to admin accounts page' do
delete :destroy, params: { user_id: user.id }
context 'when user has OTP enabled' do
before do
user.update(otp_required_for_login: true)
end
user.reload
expect(user.otp_required_for_login).to eq false
expect(response).to redirect_to(admin_accounts_path)
it 'redirects to admin accounts page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_enabled?).to eq false
expect(response).to redirect_to(admin_accounts_path)
end
end
context 'when user has OTP and WebAuthn enabled' do
let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') }
before do
user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id)
public_key_credential = WebAuthn::Credential.from_create(fake_client.create)
Fabricate(:webauthn_credential,
user_id: user.id,
external_id: public_key_credential.id,
public_key: public_key_credential.public_key,
nickname: 'Security Key')
end
it 'redirects to admin accounts page' do
delete :destroy, params: { user_id: user.id }
user.reload
expect(user.otp_enabled?).to eq false
expect(user.webauthn_enabled?).to eq false
expect(response).to redirect_to(admin_accounts_path)
end
end
end
end

View File

@ -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
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")
expect(controller).to render_template(partial: "_otp_authentication_form")
end
end
context 'using upcase email and password' do
before do
post :create, params: { user: { email: user.email.upcase, password: user.password } }
end
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
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
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
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
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 upcase email and password' do
before do
post :create, params: { user: { email: user.email.upcase, password: user.password } }
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 'renders two factor authentication page' do
expect(controller).to render_template("two_factor")
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 }
let!(:recovery_codes) do
codes = user.generate_otp_backup_codes!
user.save
return codes
end
it 'redirects to home' do
expect(response).to redirect_to(root_path)
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
it 'logs the user in' do
expect(controller.current_user).to eq user
end
end
let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http' }://#{Rails.configuration.x.web_domain}" }
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 }
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
end
end
context 'using a valid webauthn credential' do
before do
@controller.session[:webauthn_challenge] = challenge
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
post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id }
end
it 'redirects to home' do
expect(response).to redirect_to(root_path)
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
end
it 'logs the user in' do
expect(controller.current_user).to eq user
end
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
it 'updates the sign count' do
expect(webauthn_credential.reload.sign_count).to eq(sign_count)
end
end
end
end

View File

@ -5,8 +5,6 @@ require 'rails_helper'
describe Settings::TwoFactorAuthentication::ConfirmationsController do
render_views
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: 'thisisasecretforthespecofnewview') }
let(:user_without_otp_secret) { Fabricate(:user, email: 'local-part@domain') }
shared_examples 'renders :new' do
it 'renders the new view' do
@ -20,87 +18,101 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
end
end
describe 'GET #new' do
context 'when signed in' do
subject do
sign_in user, scope: :user
get :new, session: { challenge_passed_at: Time.now.utc }
end
[true, false].each do |with_otp_secret|
let(:user) { Fabricate(:user, email: 'local-part@domain', otp_secret: with_otp_secret ? 'oldotpsecret' : nil) }
include_examples 'renders :new'
end
it 'redirects if not signed in' do
get :new
expect(response).to redirect_to('/auth/sign_in')
end
it 'redirects if user do not have otp_secret' do
sign_in user_without_otp_secret, scope: :user
get :new, session: { challenge_passed_at: Time.now.utc }
expect(response).to redirect_to('/settings/two_factor_authentication')
end
end
describe 'POST #create' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when form_two_factor_confirmation parameter is not provided' do
it 'raises ActionController::ParameterMissing' do
post :create, params: {}, session: { challenge_passed_at: Time.now.utc }
expect(response).to have_http_status(400)
end
end
describe 'when creation succeeds' do
it 'renders page with success' do
otp_backup_codes = user.generate_otp_backup_codes!
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
expect(value).to eq user
otp_backup_codes
end
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
expect(value).to eq user
expect(arg).to eq '123456'
true
end
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, session: { challenge_passed_at: Time.now.utc }
expect(assigns(:recovery_codes)).to eq otp_backup_codes
expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
end
end
describe 'when creation fails' do
describe 'GET #new' do
context 'when signed in and a new otp secret has been setted in the session' do
subject do
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
expect(value).to eq user
expect(arg).to eq '123456'
false
end
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }, session: { challenge_passed_at: Time.now.utc }
end
it 'renders the new view' do
subject
expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
sign_in user, scope: :user
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end
include_examples 'renders :new'
end
it 'redirects if not signed in' do
get :new
expect(response).to redirect_to('/auth/sign_in')
end
it 'redirects if a new otp_secret has not been setted in the session' do
sign_in user, scope: :user
get :new, session: { challenge_passed_at: Time.now.utc }
expect(response).to redirect_to('/settings/otp_authentication')
end
end
context 'when not signed in' do
it 'redirects if not signed in' do
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
expect(response).to redirect_to('/auth/sign_in')
describe 'POST #create' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when form_two_factor_confirmation parameter is not provided' do
it 'raises ActionController::ParameterMissing' do
post :create, params: {}, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
expect(response).to have_http_status(400)
end
end
describe 'when creation succeeds' do
it 'renders page with success' do
otp_backup_codes = user.generate_otp_backup_codes!
expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
expect(value).to eq user
otp_backup_codes
end
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
expect(value).to eq user
expect(code).to eq '123456'
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
true
end
expect do
post :create,
params: { form_two_factor_confirmation: { otp_attempt: '123456' } },
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end.to change { user.reload.otp_secret }.to 'thisisasecretforthespecofnewview'
expect(assigns(:recovery_codes)).to eq otp_backup_codes
expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled'
expect(response).to have_http_status(200)
expect(response).to render_template('settings/two_factor_authentication/recovery_codes/index')
end
end
describe 'when creation fails' do
subject do
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
expect(value).to eq user
expect(code).to eq '123456'
expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
false
end
expect do
post :create,
params: { form_two_factor_confirmation: { otp_attempt: '123456' } },
session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end.to not_change { user.reload.otp_secret }
end
it 'renders the new view' do
subject
expect(response.body).to include 'The entered code was invalid! Are server time and device time correct?'
end
include_examples 'renders :new'
end
end
context 'when not signed in' do
it 'redirects if not signed in' do
post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
expect(response).to redirect_to('/auth/sign_in')
end
end
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
require 'rails_helper'
describe Settings::TwoFactorAuthentication::OtpAuthenticationController do
render_views
let(:user) { Fabricate(:user) }
describe 'GET #show' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when user has OTP enabled' do
before do
user.update(otp_required_for_login: true)
end
it 'redirects to two factor authentciation methods list page' do
get :show
expect(response).to redirect_to settings_two_factor_authentication_methods_path
end
end
describe 'when user does not have OTP enabled' do
before do
user.update(otp_required_for_login: false)
end
it 'returns http success' do
get :show
expect(response).to have_http_status(200)
end
end
end
context 'when not signed in' do
it 'redirects' do
get :show
expect(response).to redirect_to new_user_session_path
end
end
end
describe 'POST #create' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when user has OTP enabled' do
before do
user.update(otp_required_for_login: true)
end
describe 'when creation succeeds' do
it 'redirects to code confirmation page without updating user secret and setting otp secret in the session' do
expect do
post :create, session: { challenge_passed_at: Time.now.utc }
end.to not_change { user.reload.otp_secret }
.and change { session[:new_otp_secret] }
expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
end
end
end
describe 'when user does not have OTP enabled' do
before do
user.update(otp_required_for_login: false)
end
describe 'when creation succeeds' do
it 'redirects to code confirmation page without updating user secret and setting otp secret in the session' do
expect do
post :create, session: { challenge_passed_at: Time.now.utc }
end.to not_change { user.reload.otp_secret }
.and change { session[:new_otp_secret] }
expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
end
end
end
end
context 'when not signed in' do
it 'redirects to login' do
get :show
expect(response).to redirect_to new_user_session_path
end
end
end
end

View File

@ -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

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'rails_helper'
describe Settings::TwoFactorAuthenticationMethodsController do
render_views
let(:user) { Fabricate(:user) }
describe 'GET #index' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when user has enabled otp' do
before do
user.update(otp_required_for_login: true)
end
it 'returns http success' do
get :index
expect(response).to have_http_status(200)
end
end
describe 'when user has not enabled otp' do
before do
user.update(otp_required_for_login: false)
end
it 'redirects to enable otp' do
get :index
expect(response).to redirect_to(settings_otp_authentication_path)
end
end
end
context 'when not signed in' do
it 'redirects' do
get :index
expect(response).to redirect_to '/auth/sign_in'
end
end
end
end

View File

@ -1,125 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Settings::TwoFactorAuthenticationsController do
render_views
let(:user) { Fabricate(:user) }
describe 'GET #show' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when user requires otp for login already' do
it 'returns http success' do
user.update(otp_required_for_login: true)
get :show
expect(response).to have_http_status(200)
end
end
describe 'when user does not require otp for login' do
it 'returns http success' do
user.update(otp_required_for_login: false)
get :show
expect(response).to have_http_status(200)
end
end
end
context 'when not signed in' do
it 'redirects' do
get :show
expect(response).to redirect_to '/auth/sign_in'
end
end
end
describe 'POST #create' do
context 'when signed in' do
before do
sign_in user, scope: :user
end
describe 'when user requires otp for login already' do
it 'redirects to show page' do
user.update(otp_required_for_login: true)
post :create
expect(response).to redirect_to(settings_two_factor_authentication_path)
end
end
describe 'when creation succeeds' do
it 'updates user secret' do
before = user.otp_secret
post :create, session: { challenge_passed_at: Time.now.utc }
expect(user.reload.otp_secret).not_to eq(before)
expect(response).to redirect_to(new_settings_two_factor_authentication_confirmation_path)
end
end
end
context 'when not signed in' do
it 'redirects' do
get :show
expect(response).to redirect_to '/auth/sign_in'
end
end
end
describe 'POST #destroy' do
before do
user.update(otp_required_for_login: true)
end
context 'when signed in' do
before do
sign_in user, scope: :user
end
it 'turns off otp requirement with correct code' do
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
expect(value).to eq user
expect(arg).to eq '123456'
true
end
post :destroy, params: { form_two_factor_confirmation: { otp_attempt: '123456' } }
expect(response).to redirect_to(settings_two_factor_authentication_path)
user.reload
expect(user.otp_required_for_login).to eq(false)
end
it 'does not turn off otp if code is incorrect' do
expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, arg|
expect(value).to eq user
expect(arg).to eq '057772'
false
end
post :destroy, params: { form_two_factor_confirmation: { otp_attempt: '057772' } }
user.reload
expect(user.otp_required_for_login).to eq(true)
end
it 'raises ActionController::ParameterMissing if code is missing' do
post :destroy
expect(response).to have_http_status(400)
end
end
it 'redirects if not signed in' do
get :show
expect(response).to redirect_to '/auth/sign_in'
end
end
end

View File

@ -0,0 +1,7 @@
Fabricator(:webauthn_credential) do
user_id { Fabricate(:user).id }
external_id { Base64.urlsafe_encode64(SecureRandom.random_bytes(16)) }
public_key { OpenSSL::PKey::EC.new("prime256v1").generate_key.public_key }
nickname 'USB key'
sign_count 0
end

View File

@ -73,6 +73,26 @@ RSpec.describe ActivityPub::Activity::Announce do
expect(sender.reblogged?(sender.statuses.first)).to be true
end
end
context 'self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do
let(:object_json) do
{
id: 'https://example.com/actor#bar',
type: 'Note',
content: 'Lorem ipsum',
attributedTo: 'https://example.com/actor',
to: {
'type': 'OrderedCollection',
'id': 'http://example.com/followers',
'first': 'http://example.com/followers?page=true',
}
}
end
it 'creates a reblog by sender of status' do
expect(sender.reblogged?(sender.statuses.first)).to be true
end
end
end
context 'when the status belongs to a local user' do

View File

@ -121,6 +121,28 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'private with inlined Collection in audience' do
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: {
'type': 'OrderedCollection',
'id': 'http://example.com/followers',
'first': 'http://example.com/followers?page=true',
}
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'private'
end
end
context 'limited' do
let(:recipient) { Fabricate(:account) }

View File

@ -0,0 +1,73 @@
require 'rails_helper'
RSpec.describe ActivityPub::Dereferencer do
describe '#object' do
let(:object) { { '@context': 'https://www.w3.org/ns/activitystreams', id: 'https://example.com/foo', type: 'Note', content: 'Hoge' } }
let(:permitted_origin) { 'https://example.com' }
let(:signature_account) { nil }
let(:uri) { nil }
subject { described_class.new(uri, permitted_origin: permitted_origin, signature_account: signature_account).object }
before do
stub_request(:get, 'https://example.com/foo').to_return(body: Oj.dump(object), headers: { 'Content-Type' => 'application/activity+json' })
end
context 'with a URI' do
let(:uri) { 'https://example.com/foo' }
it 'returns object' do
expect(subject.with_indifferent_access).to eq object.with_indifferent_access
end
context 'with signature account' do
let(:signature_account) { Fabricate(:account) }
it 'makes signed request' do
subject
expect(a_request(:get, 'https://example.com/foo').with { |req| req.headers['Signature'].present? }).to have_been_made
end
end
context 'with different origin' do
let(:uri) { 'https://other-example.com/foo' }
it 'does not make request' do
subject
expect(a_request(:get, 'https://other-example.com/foo')).to_not have_been_made
end
end
end
context 'with a bearcap' do
let(:uri) { 'bear:?t=hoge&u=https://example.com/foo' }
it 'makes request with Authorization header' do
subject
expect(a_request(:get, 'https://example.com/foo').with(headers: { 'Authorization' => 'Bearer hoge' })).to have_been_made
end
it 'returns object' do
expect(subject.with_indifferent_access).to eq object.with_indifferent_access
end
context 'with signature account' do
let(:signature_account) { Fabricate(:account) }
it 'makes signed request' do
subject
expect(a_request(:get, 'https://example.com/foo').with { |req| req.headers['Signature'].present? && req.headers['Authorization'] == 'Bearer hoge' }).to have_been_made
end
end
context 'with different origin' do
let(:uri) { 'bear:?t=hoge&u=https://other-example.com/foo' }
it 'does not make request' do
subject
expect(a_request(:get, 'https://other-example.com/foo')).to_not have_been_made
end
end
end
end
end

View File

@ -33,6 +33,28 @@ class UserMailerPreview < ActionMailer::Preview
UserMailer.two_factor_recovery_codes_changed(User.first)
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_enabled
def webauthn_enabled
UserMailer.webauthn_enabled(User.first)
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_disabled
def webauthn_disabled
UserMailer.webauthn_disabled(User.first)
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_credential_added
def webauthn_credential_added
webauthn_credential = WebauthnCredential.new(nickname: 'USB Key')
UserMailer.webauthn_credential_added(User.first, webauthn_credential)
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/webauthn_credential_deleted
def webauthn_credential_deleted
webauthn_credential = WebauthnCredential.new(nickname: 'USB Key')
UserMailer.webauthn_credential_deleted(User.first, webauthn_credential)
end
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/reconfirmation_instructions
def reconfirmation_instructions
user = User.first

View File

@ -151,6 +151,12 @@ RSpec.describe User, type: :model do
expect(user.reload.otp_required_for_login).to be false
end
it 'saves nil for otp_secret' do
user = Fabricate.build(:user, otp_secret: 'oldotpcode')
user.disable_two_factor!
expect(user.reload.otp_secret).to be nil
end
it 'saves cleared otp_backup_codes' do
user = Fabricate.build(:user, otp_backup_codes: %w(dummy dummy))
user.disable_two_factor!

View File

@ -0,0 +1,80 @@
require 'rails_helper'
RSpec.describe WebauthnCredential, type: :model do
describe 'validations' do
it 'is invalid without an external id' do
webauthn_credential = Fabricate.build(:webauthn_credential, external_id: nil)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:external_id)
end
it 'is invalid without a public key' do
webauthn_credential = Fabricate.build(:webauthn_credential, public_key: nil)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:public_key)
end
it 'is invalid without a nickname' do
webauthn_credential = Fabricate.build(:webauthn_credential, nickname: nil)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:nickname)
end
it 'is invalid without a sign_count' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: nil)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
it 'is invalid if already exist a webauthn credential with the same external id' do
existing_webauthn_credential = Fabricate(:webauthn_credential, external_id: "_Typ0ygudDnk9YUVWLQayw")
new_webauthn_credential = Fabricate.build(:webauthn_credential, external_id: "_Typ0ygudDnk9YUVWLQayw")
new_webauthn_credential.valid?
expect(new_webauthn_credential).to model_have_error_on_field(:external_id)
end
it 'is invalid if user already registered a webauthn credential with the same nickname' do
user = Fabricate(:user)
existing_webauthn_credential = Fabricate(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
new_webauthn_credential = Fabricate.build(:webauthn_credential, user_id: user.id, nickname: 'USB Key')
new_webauthn_credential.valid?
expect(new_webauthn_credential).to model_have_error_on_field(:nickname)
end
it 'is invalid if sign_count is not a number' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 'invalid sign_count')
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
it 'is invalid if sign_count is negative number' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: -1)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
it 'is invalid if sign_count is greater 2**32 - 1' do
webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**32)
webauthn_credential.valid?
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
end
end

View File

@ -70,6 +70,8 @@ RSpec::Sidekiq.configure do |config|
config.warn_when_jobs_not_processed_by_sidekiq = false
end
RSpec::Matchers.define_negated_matcher :not_change, :change
def request_fixture(name)
File.read(Rails.root.join('spec', 'fixtures', 'requests', name))
end