Commit d4cab271 authored by Tan Le's avatar Tan Le

Audit failed 2-factor authentication

First failed factor authentication (i.e using password) is audited but
not the second failed factor authentication. This change audits the
later. The ability to view the second authentication attempts allows
better monitoring of malicious attacks when the user's password has been
compromised.
parent a9e0629b
...@@ -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
...@@ -3676,7 +3676,10 @@ msgstr "" ...@@ -3676,7 +3676,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