Commit 5d673390 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '16826-audit-failed-2fa' into 'master'

Audit failed 2-factor login attempt

See merge request gitlab-org/gitlab!41641
parents 5fad87ba d4cab271
...@@ -89,10 +89,7 @@ module AuthenticatesWithTwoFactor ...@@ -89,10 +89,7 @@ module AuthenticatesWithTwoFactor
user.save! user.save!
sign_in(user, message: :two_factor_authenticated, event: :authentication) sign_in(user, message: :two_factor_authenticated, event: :authentication)
else else
user.increment_failed_attempts! handle_two_factor_failure(user, 'OTP', _('Invalid two-factor code.'))
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP")
flash.now[:alert] = _('Invalid two-factor code.')
prompt_for_two_factor(user)
end end
end end
...@@ -101,7 +98,7 @@ module AuthenticatesWithTwoFactor ...@@ -101,7 +98,7 @@ module AuthenticatesWithTwoFactor
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
handle_two_factor_success(user) handle_two_factor_success(user)
else else
handle_two_factor_failure(user, 'U2F') handle_two_factor_failure(user, 'U2F', _('Authentication via U2F device failed.'))
end end
end end
...@@ -109,7 +106,7 @@ module AuthenticatesWithTwoFactor ...@@ -109,7 +106,7 @@ module AuthenticatesWithTwoFactor
if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
handle_two_factor_success(user) handle_two_factor_success(user)
else else
handle_two_factor_failure(user, 'WebAuthn') handle_two_factor_failure(user, 'WebAuthn', _('Authentication via WebAuthn device failed.'))
end end
end end
...@@ -152,13 +149,19 @@ module AuthenticatesWithTwoFactor ...@@ -152,13 +149,19 @@ module AuthenticatesWithTwoFactor
sign_in(user, message: :two_factor_authenticated, event: :authentication) sign_in(user, message: :two_factor_authenticated, event: :authentication)
end end
def handle_two_factor_failure(user, method) def handle_two_factor_failure(user, method, message)
user.increment_failed_attempts! user.increment_failed_attempts!
log_failed_two_factor(user, method, request.remote_ip)
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}") Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method } flash.now[:alert] = message
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
def log_failed_two_factor(user, method, ip_address)
# overridden in EE
end
def handle_changed_user(user) def handle_changed_user(user)
clear_two_factor_attempt! clear_two_factor_attempt!
...@@ -173,3 +176,5 @@ module AuthenticatesWithTwoFactor ...@@ -173,3 +176,5 @@ module AuthenticatesWithTwoFactor
Digest::SHA256.hexdigest(user.encrypted_password) != session[:user_password_hash] Digest::SHA256.hexdigest(user.encrypted_password) != session[:user_password_hash]
end end
end end
AuthenticatesWithTwoFactor.prepend_if_ee('EE::AuthenticatesWithTwoFactor')
...@@ -52,11 +52,7 @@ module AuthenticatesWithTwoFactorForAdminMode ...@@ -52,11 +52,7 @@ module AuthenticatesWithTwoFactorForAdminMode
# The admin user has successfully passed 2fa, enable admin mode ignoring password # The admin user has successfully passed 2fa, enable admin mode ignoring password
enable_admin_mode enable_admin_mode
else else
user.increment_failed_attempts! admin_handle_two_factor_failure(user, 'OTP', _('Invalid two-factor code.'))
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=OTP")
flash.now[:alert] = _('Invalid two-factor code.')
admin_mode_prompt_for_two_factor(user)
end end
end end
...@@ -64,7 +60,7 @@ module AuthenticatesWithTwoFactorForAdminMode ...@@ -64,7 +60,7 @@ module AuthenticatesWithTwoFactorForAdminMode
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge]) if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
admin_handle_two_factor_success admin_handle_two_factor_success
else else
admin_handle_two_factor_failure(user, 'U2F') admin_handle_two_factor_failure(user, 'U2F', _('Authentication via U2F device failed.'))
end end
end end
...@@ -72,7 +68,7 @@ module AuthenticatesWithTwoFactorForAdminMode ...@@ -72,7 +68,7 @@ module AuthenticatesWithTwoFactorForAdminMode
if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
admin_handle_two_factor_success admin_handle_two_factor_success
else else
admin_handle_two_factor_failure(user, 'WebAuthn') admin_handle_two_factor_failure(user, 'WebAuthn', _('Authentication via WebAuthn device failed.'))
end end
end end
...@@ -100,11 +96,12 @@ module AuthenticatesWithTwoFactorForAdminMode ...@@ -100,11 +96,12 @@ module AuthenticatesWithTwoFactorForAdminMode
enable_admin_mode enable_admin_mode
end end
def admin_handle_two_factor_failure(user, method) def admin_handle_two_factor_failure(user, method, message)
user.increment_failed_attempts! user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}") log_failed_two_factor(user, method, request.remote_ip)
flash.now[:alert] = _('Authentication via %{method} device failed.') % { method: method }
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
flash.now[:alert] = message
admin_mode_prompt_for_two_factor(user) admin_mode_prompt_for_two_factor(user)
end end
end end
...@@ -126,6 +126,7 @@ recorded: ...@@ -126,6 +126,7 @@ recorded:
- User was added ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/251) in GitLab 12.8) - User was added ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/251) in GitLab 12.8)
- User was blocked via Admin Area ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/251) in GitLab 12.8) - User was blocked via Admin Area ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/251) in GitLab 12.8)
- User was blocked via API ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25872) in GitLab 12.9) - User was blocked via API ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25872) in GitLab 12.9)
- Failed second-factor authentication attempt ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16826) in GitLab 13.5)
It's possible to filter particular actions by choosing an audit data type from It's possible to filter particular actions by choosing an audit data type from
the filter dropdown box. You can further filter by specific group, project, or user the filter dropdown box. You can further filter by specific group, project, or user
...@@ -172,6 +173,7 @@ the steps bellow. ...@@ -172,6 +173,7 @@ the steps bellow.
```ruby ```ruby
Feature.enable(:repository_push_audit_event) Feature.enable(:repository_push_audit_event)
```
## Export to CSV **(PREMIUM ONLY)** ## Export to CSV **(PREMIUM ONLY)**
......
# frozen_string_literal: true
module EE
module AuthenticatesWithTwoFactor
extend ::Gitlab::Utils::Override
override :log_failed_two_factor
def log_failed_two_factor(user, method, ip_address)
::AuditEventService.new(
user,
user,
ip_address: ip_address,
with: method
).for_failed_login.unauth_security_event
end
end
end
---
title: Audit failed 2-factor login attempt
merge_request: 41641
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do
include_context 'custom session'
describe '#create' do
context 'when using two-factor authentication' do
def authenticate_2fa(user_params, otp_user_id: user.id)
post(:create, params: { user: user_params }, session: { otp_user_id: otp_user_id })
end
before do
sign_in(user)
controller.current_user_mode.request_admin_mode!
end
context 'when OTP authentication fails' do
it_behaves_like 'an auditable failed authentication' do
let(:user) { create(:admin, :two_factor) }
let(:operation) { authenticate_2fa(otp_attempt: 'invalid', otp_user_id: user.id) }
let(:method) { 'OTP' }
end
end
context 'when U2F authentication fails' do
before do
allow(U2fRegistration).to receive(:authenticate).and_return(false)
end
it_behaves_like 'an auditable failed authentication' do
let(:user) { create(:admin, :two_factor_via_u2f) }
let(:operation) { authenticate_2fa(device_response: 'invalid', otp_user_id: user.id) }
let(:method) { 'U2F' }
end
end
context 'when WebAuthn authentication fails' do
before do
stub_feature_flags(webauthn: true)
webauthn_authenticate_service = instance_spy(Webauthn::AuthenticateService, execute: false)
allow(Webauthn::AuthenticateService).to receive(:new).and_return(webauthn_authenticate_service)
end
it_behaves_like 'an auditable failed authentication' do
let(:user) { create(:admin, :two_factor_via_webauthn) }
let(:operation) { authenticate_2fa(device_response: 'invalid', otp_user_id: user.id) }
let(:method) { 'WebAuthn' }
end
end
end
end
end
...@@ -88,5 +88,45 @@ RSpec.describe SessionsController, :geo do ...@@ -88,5 +88,45 @@ RSpec.describe SessionsController, :geo do
end end
end end
end end
context 'when using two-factor authentication' do
def authenticate_2fa(user_params, otp_user_id: user.id)
post(:create, params: { user: user_params }, session: { otp_user_id: otp_user_id })
end
context 'when OTP authentication fails' do
it_behaves_like 'an auditable failed authentication' do
let(:user) { create(:user, :two_factor) }
let(:operation) { authenticate_2fa(otp_attempt: 'invalid', otp_user_id: user.id) }
let(:method) { 'OTP' }
end
end
context 'when U2F authentication fails' do
before do
allow(U2fRegistration).to receive(:authenticate).and_return(false)
end
it_behaves_like 'an auditable failed authentication' do
let(:user) { create(:user, :two_factor_via_u2f) }
let(:operation) { authenticate_2fa(device_response: 'invalid', otp_user_id: user.id) }
let(:method) { 'U2F' }
end
end
context 'when WebAuthn authentication fails' do
before do
stub_feature_flags(webauthn: true)
webauthn_authenticate_service = instance_spy(Webauthn::AuthenticateService, execute: false)
allow(Webauthn::AuthenticateService).to receive(:new).and_return(webauthn_authenticate_service)
end
it_behaves_like 'an auditable failed authentication' do
let(:user) { create(:user, :two_factor_via_webauthn) }
let(:operation) { authenticate_2fa(device_response: 'invalid', otp_user_id: user.id) }
let(:method) { 'WebAuthn' }
end
end
end
end end
end end
...@@ -32,6 +32,16 @@ RSpec.describe 'Login' do ...@@ -32,6 +32,16 @@ RSpec.describe 'Login' do
.to change { AuditEvent.where(entity_id: -1).count }.from(0).to(1) .to change { AuditEvent.where(entity_id: -1).count }.from(0).to(1)
end end
it 'creates a security event for an invalid one-time code' do
user = create(:user, :two_factor)
gitlab_sign_in(user)
expect do
fill_in 'user_otp_attempt', with: 'invalid_code'
click_button 'Verify code'
end.to change { AuditEvent.count }.by(1)
end
describe 'smartcard authentication' do describe 'smartcard authentication' do
before do before do
allow(Gitlab.config.smartcard).to receive(:enabled).and_return(true) allow(Gitlab.config.smartcard).to receive(:enabled).and_return(true)
......
# frozen_string_literal: true
RSpec.shared_examples 'an auditable failed authentication' do
it 'log an audit event', :aggregate_failures do
audit_event_service = instance_spy(AuditEventService)
allow(AuditEventService).to receive(:new).and_return(audit_event_service)
operation
expect(AuditEventService).to have_received(:new).with(user, user, ip_address: '0.0.0.0', with: method)
expect(audit_event_service).to have_received(:for_failed_login)
expect(audit_event_service).to have_received(:unauth_security_event)
end
end
...@@ -3691,7 +3691,10 @@ msgstr "" ...@@ -3691,7 +3691,10 @@ msgstr ""
msgid "Authentication method updated" msgid "Authentication method updated"
msgstr "" msgstr ""
msgid "Authentication via %{method} device failed." msgid "Authentication via U2F device failed."
msgstr ""
msgid "Authentication via WebAuthn device failed."
msgstr "" msgstr ""
msgid "Author" msgid "Author"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment