123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- # Login and logout work only via controller, so use type: request.
- RSpec.describe Gql::Mutations::Login, :aggregate_failures, type: :request do
- before do
- stub_const('Auth::BRUTE_FORCE_SLEEP', 0)
- end
- context 'when logging on' do
- let(:agent_password) { 'some_test_password' }
- let(:agent) { create(:agent, password: agent_password) }
- let(:query) do
- <<~QUERY
- mutation login($input: LoginInput!) {
- login(input: $input) {
- session {
- id
- afterAuth {
- type
- data
- }
- }
- twoFactorRequired {
- defaultTwoFactorAuthenticationMethod
- availableTwoFactorAuthenticationMethods
- recoveryCodesAvailable
- }
- errors {
- message
- field
- }
- }
- }
- QUERY
- end
- let(:password) { agent_password }
- let(:fingerprint) { Faker::Number.unique.number(digits: 6).to_s }
- let(:two_factor_authentication) { nil }
- let(:two_factor_recovery) { nil }
- let(:variables) do
- {
- input: {
- login: agent.login,
- password: password,
- twoFactorAuthentication: two_factor_authentication,
- twoFactorRecovery: two_factor_recovery
- }
- }
- end
- let(:headers) do
- {
- 'X-Browser-Fingerprint' => fingerprint,
- }
- end
- let(:graphql_response) do
- execute_graphql_query
- json_response
- end
- def execute_graphql_query
- post '/graphql', params: { query: query, variables: variables }, headers: headers, as: :json
- end
- context 'with correct credentials' do
- context 'without two factor authentication' do
- it 'returns session data' do
- expect(graphql_response['data']['login']['session']['id']).to be_present
- end
- it 'sets the :persistent session parameter' do
- expect { execute_graphql_query }.to change { request&.session&.fetch(:persistent) }.to(true)
- end
- it 'adds an activity stream entry for the user’s session' do
- # Create the user before the GraphQL query execution, so that we have only the activity stream
- # change from the login.
- agent
- expect { execute_graphql_query }.to change(ActivityStream, :count).by(1)
- end
- context 'with remember me' do
- let(:remember_me) { true }
- let(:variables) do
- {
- input: {
- login: agent.login,
- password: password,
- rememberMe: remember_me,
- }
- }
- end
- it 'adds an activity stream entry for the user’s session' do
- execute_graphql_query
- expect(request.env['rack.session.options'][:expire_after]).to eq(1.year)
- end
- context 'with not activated remember me' do
- let(:remember_me) { false }
- it 'adds an activity stream entry for the user’s session' do
- execute_graphql_query
- expect(request.env['rack.session.options'][:expire_after]).to be_nil
- end
- end
- end
- end
- context 'with after_auth information' do
- before do
- allow_any_instance_of(Auth::AfterAuth::TwoFactorConfiguration).to receive(:check).and_return(true)
- end
- it 'returns after_auth data' do
- expect(graphql_response['data']['login']['session']['afterAuth']).to eq({ 'type' => 'TwoFactorConfiguration', 'data' => {} })
- end
- end
- context 'with two factor authentication' do
- let!(:two_factor_pref) { create(:user_two_factor_preference, :authenticator_app, user: agent) }
- let(:enabled) { true }
- before do
- Setting.set('two_factor_authentication_method_authenticator_app', enabled)
- end
- context 'without token' do
- it 'returns two factor availability data' do
- expect(graphql_response['data']['login']['twoFactorRequired']).to eq(
- {
- 'defaultTwoFactorAuthenticationMethod' => 'authenticator_app',
- 'availableTwoFactorAuthenticationMethods' => ['authenticator_app'],
- 'recoveryCodesAvailable' => false
- }
- )
- expect(graphql_response['data']['login']['session']).not_to be_present
- end
- end
- context 'with wrong token' do
- let(:two_factor_authentication) do
- {
- twoFactorMethod: :authenticator_app,
- twoFactorPayload: 'wrong'
- }
- end
- it 'fails with error message' do
- expect(graphql_response['data']['login']['errors']).to eq(
- [{
- 'message' => 'Login failed. Please double-check your two-factor authentication method.',
- 'field' => nil
- }]
- )
- end
- context 'when default method is disabled, but another exists' do
- let(:enabled) { true }
- before do
- two_factor_pref
- create(:user_two_factor_preference, :security_keys, user: agent)
- agent.reload
- agent.preferences[:two_factor_authentication][:default] = 'security_keys'
- agent.save!
- end
- it 'fails with error message' do
- expect(graphql_response['data']['login']['errors']).to eq(
- [{
- 'message' => 'Login failed. Please double-check your two-factor authentication method.',
- 'field' => nil
- }]
- )
- end
- end
- context 'with disabled authenticator method' do
- let(:enabled) { false }
- it 'returns session data' do
- expect(graphql_response['data']['login']['session']['id']).to be_present
- end
- end
- end
- context 'with correct token' do
- let(:two_factor_authentication) do
- {
- twoFactorMethod: :authenticator_app,
- twoFactorPayload: two_factor_pref.configuration[:code]
- }
- end
- it 'returns session data' do
- expect(graphql_response['data']['login']['session']['id']).to be_present
- end
- context 'with disabled authenticator method' do
- let(:enabled) { false }
- it 'returns session data' do
- expect(graphql_response['data']['login']['session']['id']).to be_present
- end
- end
- end
- end
- end
- context 'without CSRF token', allow_forgery_protection: true do
- it 'fails with error message' do
- expect(graphql_response['errors'][0]).to include('message' => 'CSRF token verification failed!')
- end
- it 'fails with error type' do
- expect(graphql_response['errors'][0]['extensions']).to include({ 'type' => 'Exceptions::NotAuthorized' })
- end
- end
- context 'with wrong password' do
- let(:password) { 'wrong' }
- it 'fails with error message' do
- expect(graphql_response['data']['login']['errors']).to eq(
- [{
- 'message' => 'Login failed. Have you double-checked your credentials and completed the email verification step?',
- 'field' => nil
- }]
- )
- end
- end
- context 'without fingerprint' do
- let(:fingerprint) { nil }
- it 'fails with error message' do
- expect(graphql_response['errors'][0]).to include('message' => 'Need fingerprint param!')
- end
- # No error type available for GraphQL::ExecutionErrors.
- end
- context 'with fingerprint' do
- let(:fingerprint) { 'my_finger_print' }
- it 'exists in controller session' do
- execute_graphql_query
- expect(controller.session[:user_device_fingerprint]).to eq('my_finger_print')
- end
- it 'added user device log entry', :performs_jobs do
- perform_enqueued_jobs only: UserDeviceLogJob do
- execute_graphql_query
- end
- expect(UserDevice.where(user_id: agent.id).count).to eq(1)
- end
- end
- end
- end
|