Welcome to mirror list, hosted at ThFree Co, Russian Federation.

email_verification_on_login_spec.rb « users « features « spec - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: f7102eaf9b7437bf35d7cd30ab358bf4b7f995fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting do
  include EmailHelpers

  let_it_be(:user) { create(:user) }

  let(:require_email_verification_enabled) { user }

  before do
    stub_feature_flags(require_email_verification: require_email_verification_enabled)
  end

  shared_examples 'email verification required' do
    before do
      allow(Gitlab::AppLogger).to receive(:info)
    end

    it 'requires email verification before being able to access GitLab' do
      perform_enqueued_jobs do
        # When logging in
        gitlab_sign_in(user)
        expect_log_message(message: "Account Locked: username=#{user.username}")
        expect_log_message('Instructions Sent')

        # Expect the user to be locked and the unlock_token to be set
        user.reload
        expect(user.locked_at).not_to be_nil
        expect(user.unlock_token).not_to be_nil

        # Expect to see the verification form on the login page
        expect(page).to have_current_path(new_user_session_path)
        expect(page).to have_content('Help us protect your account')

        # Expect an instructions email to be sent with a code
        code = expect_instructions_email_and_extract_code

        # Signing in again prompts for the code and doesn't send a new one
        gitlab_sign_in(user)
        expect(page).to have_current_path(new_user_session_path)
        expect(page).to have_content('Help us protect your account')

        # Verify the code
        verify_code(code)
        expect_log_message('Successful')
        expect_log_message(message: "Successful Login: username=#{user.username} "\
                                    "ip=127.0.0.1 method=standard admin=false")

        # Expect the user to be unlocked
        expect_user_to_be_unlocked

        # Expect a confirmation page with a meta refresh tag for 3 seconds to the root
        expect(page).to have_current_path(users_successful_verification_path)
        expect(page).to have_content('Verification successful')
        expect(page).to have_selector("meta[http-equiv='refresh'][content='3; url=#{root_path}']", visible: false)
      end
    end

    describe 'resending a new code' do
      it 'resends a new code' do
        perform_enqueued_jobs do
          # When logging in
          gitlab_sign_in(user)

          # Expect an instructions email to be sent with a code
          code = expect_instructions_email_and_extract_code

          # Request a new code
          click_link 'Resend code'
          expect_log_message('Instructions Sent', 2)
          new_code = expect_instructions_email_and_extract_code

          # Verify the old code is different from the new code
          expect(code).not_to eq(new_code)
        end
      end

      it 'rate limits resends' do
        # When logging in
        gitlab_sign_in(user)

        # It shows a resend button
        expect(page).to have_link 'Resend code'

        # Resend more than the rate limited amount of times
        10.times do
          click_link 'Resend code'
        end

        # Expect the link to be gone
        expect(page).not_to have_link 'Resend code'

        # Wait for 1 hour
        travel 1.hour

        # Now it's visible again
        gitlab_sign_in(user)
        expect(page).to have_link 'Resend code'
      end
    end

    describe 'verification errors' do
      it 'rate limits verifications' do
        perform_enqueued_jobs do
          # When logging in
          gitlab_sign_in(user)

          # Expect an instructions email to be sent with a code
          code = expect_instructions_email_and_extract_code

          # Verify an invalid token more than the rate limited amount of times
          11.times do
            verify_code('123456')
          end

          # Expect an error message
          expect_log_message('Failed Attempt', reason: 'rate_limited')
          expect(page).to have_content("You've reached the maximum amount of tries. "\
                                       'Wait 10 minutes or send a new code and try again.')

          # Wait for 10 minutes
          travel 10.minutes

          # Now it works again
          verify_code(code)
          expect_log_message('Successful')
        end
      end

      it 'verifies invalid codes' do
        # When logging in
        gitlab_sign_in(user)

        # Verify an invalid code
        verify_code('123456')

        # Expect an error message
        expect_log_message('Failed Attempt', reason: 'invalid')
        expect(page).to have_content('The code is incorrect. Enter it again, or send a new code.')
      end

      it 'verifies expired codes' do
        perform_enqueued_jobs do
          # When logging in
          gitlab_sign_in(user)

          # Expect an instructions email to be sent with a code
          code = expect_instructions_email_and_extract_code

          # Wait for the code to expire before verifying
          travel Users::EmailVerification::ValidateTokenService::TOKEN_VALID_FOR_MINUTES.minutes + 1.second
          verify_code(code)

          # Expect an error message
          expect_log_message('Failed Attempt', reason: 'expired')
          expect(page).to have_content('The code has expired. Send a new code and try again.')
        end
      end
    end
  end

  shared_examples 'no email verification required' do |**login_args|
    it 'does not lock the user and redirects to the root page after logging in' do
      gitlab_sign_in(user, **login_args)

      expect_user_to_be_unlocked

      expect(page).to have_current_path(root_path)
    end
  end

  shared_examples 'no email verification required when 2fa enabled or ff disabled' do
    context 'when 2FA is enabled' do
      let_it_be(:user) { create(:user, :two_factor) }

      it_behaves_like 'no email verification required', two_factor_auth: true
    end

    context 'when the feature flag is disabled' do
      let(:require_email_verification_enabled) { false }

      it_behaves_like 'no email verification required'
    end
  end

  describe 'when failing to login the maximum allowed number of times' do
    before do
      # See comment in RequireEmailVerification::MAXIMUM_ATTEMPTS on why this is divided by 2
      (RequireEmailVerification::MAXIMUM_ATTEMPTS / 2).times do
        gitlab_sign_in(user, password: 'wrong_password')
      end
    end

    it 'locks the user, but does not set the unlock token', :aggregate_failures do
      user.reload
      expect(user.locked_at).not_to be_nil
      expect(user.unlock_token).to be_nil # The unlock token is only set after logging in with valid credentials
      expect(user.failed_attempts).to eq(RequireEmailVerification::MAXIMUM_ATTEMPTS)
    end

    it_behaves_like 'email verification required'
    it_behaves_like 'no email verification required when 2fa enabled or ff disabled'

    describe 'when waiting for the auto unlock time' do
      before do
        travel User::UNLOCK_IN + 1.second
      end

      it_behaves_like 'no email verification required'
    end
  end

  describe 'when no previous authentication event exists' do
    it_behaves_like 'no email verification required'
  end

  describe 'when a previous authentication event exists for another ip address' do
    before do
      create(:authentication_event, :successful, user: user, ip_address: '1.2.3.4')
    end

    it_behaves_like 'email verification required'
    it_behaves_like 'no email verification required when 2fa enabled or ff disabled'
  end

  describe 'when a previous authentication event exists for the same ip address' do
    before do
      create(:authentication_event, :successful, user: user)
    end

    it_behaves_like 'no email verification required'
  end

  describe 'rate limiting password guessing' do
    before do
      5.times { gitlab_sign_in(user, password: 'wrong_password') }
      gitlab_sign_in(user)
    end

    it 'shows an error message on on the login page' do
      expect(page).to have_current_path(new_user_session_path)
      expect(page).to have_content('Maximum login attempts exceeded. Wait 10 minutes and try again.')
    end
  end

  describe 'inconsistent states' do
    context 'when the feature flag is toggled off after being prompted for a verification token' do
      before do
        create(:authentication_event, :successful, user: user, ip_address: '1.2.3.4')
      end

      it 'still accepts the token' do
        perform_enqueued_jobs do
          # The user is prompted for a verification code
          gitlab_sign_in(user)
          expect(page).to have_content(s_('IdentityVerification|Help us protect your account'))
          code = expect_instructions_email_and_extract_code

          # We toggle the feature flag off
          stub_feature_flags(require_email_verification: false)

          # Resending and veryfying the code work as expected
          click_link 'Resend code'
          new_code = expect_instructions_email_and_extract_code

          verify_code(code)
          expect(page)
            .to have_content(s_('IdentityVerification|The code is incorrect. Enter it again, or send a new code.'))

          travel Users::EmailVerification::ValidateTokenService::TOKEN_VALID_FOR_MINUTES.minutes + 1.second

          verify_code(new_code)
          expect(page).to have_content(s_('IdentityVerification|The code has expired. Send a new code and try again.'))

          click_link 'Resend code'
          another_code = expect_instructions_email_and_extract_code

          verify_code(another_code)
          expect_user_to_be_unlocked
          expect(page).to have_current_path(users_successful_verification_path)
        end
      end
    end

    context 'when the feature flag is toggled on after Devise sent unlock instructions' do
      let(:require_email_verification_enabled) { false }

      before do
        perform_enqueued_jobs do
          (User.maximum_attempts / 2).times do
            gitlab_sign_in(user, password: 'wrong_password')
          end
        end
      end

      it 'the unlock link still works' do
        # The user is locked and unlock instructions are sent
        expect(page).to have_content(_('Invalid login or password.'))
        user.reload
        expect(user.locked_at).not_to be_nil
        expect(user.unlock_token).not_to be_nil
        mail = find_email_for(user)

        expect(mail.to).to match_array([user.email])
        expect(mail.subject).to eq('Unlock instructions')
        unlock_url = mail.body.parts.first.to_s[/http.*/]

        # We toggle the feature flag on
        stub_feature_flags(require_email_verification: true)

        # Unlocking works as expected
        visit unlock_url
        expect_user_to_be_unlocked
        expect(page).to have_current_path(new_user_session_path)
        expect(page).to have_content('Your account has been unlocked successfully')

        gitlab_sign_in(user)
        expect(page).to have_current_path(root_path)
      end
    end
  end

  def expect_user_to_be_unlocked
    user.reload

    aggregate_failures do
      expect(user.locked_at).to be_nil
      expect(user.unlock_token).to be_nil
      expect(user.failed_attempts).to eq(0)
    end
  end

  def expect_instructions_email_and_extract_code
    mail = find_email_for(user)
    expect(mail.to).to match_array([user.email])
    expect(mail.subject).to eq(s_('IdentityVerification|Verify your identity'))
    code = mail.body.parts.first.to_s[/\d{#{Users::EmailVerification::GenerateTokenService::TOKEN_LENGTH}}/o]
    reset_delivered_emails!
    code
  end

  def verify_code(code)
    fill_in s_('IdentityVerification|Verification code'), with: code
    click_button s_('IdentityVerification|Verify code')
  end

  def expect_log_message(event = nil, times = 1, reason: '', message: nil)
    expect(Gitlab::AppLogger).to have_received(:info)
      .exactly(times).times
      .with(message || hash_including(message: 'Email Verification',
                                      event: event,
                                      username: user.username,
                                      ip: '127.0.0.1',
                                      reason: reason))
  end
end