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
|
# frozen_string_literal: true
# == AuthenticatesWithTwoFactor
#
# Controller concern to handle two-factor authentication
module AuthenticatesWithTwoFactor
extend ActiveSupport::Concern
# Store the user's ID in the session for later retrieval and render the
# two factor code prompt
#
# The user must have been authenticated with a valid login and password
# before calling this method!
#
# user - User record
#
# Returns nil
def prompt_for_two_factor(user)
# Set @user for Devise views
@user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
return handle_locked_user(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password)
push_frontend_feature_flag(:webauthn)
if Feature.enabled?(:webauthn)
setup_webauthn_authentication(user)
else
setup_u2f_authentication(user)
end
render 'devise/sessions/two_factor'
end
def handle_locked_user(user)
clear_two_factor_attempt!
locked_user_redirect(user)
end
def locked_user_redirect(user)
redirect_to new_user_session_path, alert: locked_user_redirect_alert(user)
end
def authenticate_with_two_factor
user = self.resource = find_user
return handle_locked_user(user) unless user.can?(:log_in)
return handle_changed_user(user) if user_password_changed?(user)
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
if user.two_factor_webauthn_enabled?
authenticate_with_two_factor_via_webauthn(user)
else
authenticate_with_two_factor_via_u2f(user)
end
elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
private
def locked_user_redirect_alert(user)
if user.access_locked?
_('Your account is locked.')
elsif !user.confirmed?
I18n.t('devise.failure.unconfirmed')
else
_('Invalid login or password')
end
end
def clear_two_factor_attempt!
session.delete(:otp_user_id)
session.delete(:user_password_hash)
session.delete(:challenge)
end
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
# Remove any lingering user data from login
clear_two_factor_attempt!
remember_me(user) if user_params[:remember_me] == '1'
user.save!
sign_in(user, message: :two_factor_authenticated, event: :authentication)
else
send_two_factor_otp_attempt_failed_email(user)
handle_two_factor_failure(user, 'OTP', _('Invalid two-factor code.'))
end
end
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
handle_two_factor_success(user)
else
handle_two_factor_failure(user, 'U2F', _('Authentication via U2F device failed.'))
end
end
def authenticate_with_two_factor_via_webauthn(user)
if Webauthn::AuthenticateService.new(user, user_params[:device_response], session[:challenge]).execute
handle_two_factor_success(user)
else
handle_two_factor_failure(user, 'WebAuthn', _('Authentication via WebAuthn device failed.'))
end
end
# Setup in preparation of communication with a U2F (universal 2nd factor) device
# Actual communication is performed using a Javascript API
# rubocop: disable CodeReuse/ActiveRecord
def setup_u2f_authentication(user)
key_handles = user.u2f_registrations.pluck(:key_handle)
u2f = U2F::U2F.new(u2f_app_id)
if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
session[:challenge] ||= u2f.challenge
gon.push(u2f: { challenge: session[:challenge], app_id: u2f_app_id,
sign_requests: sign_requests })
end
end
def setup_webauthn_authentication(user)
if user.webauthn_registrations.present?
webauthn_registration_ids = user.webauthn_registrations.pluck(:credential_xid)
get_options = WebAuthn::Credential.options_for_get(allow: webauthn_registration_ids,
user_verification: 'discouraged',
extensions: { appid: WebAuthn.configuration.origin })
session[:credentialRequestOptions] = get_options
session[:challenge] = get_options.challenge
gon.push(webauthn: { options: get_options.to_json })
end
end
# rubocop: enable CodeReuse/ActiveRecord
def handle_two_factor_success(user)
# Remove any lingering user data from login
clear_two_factor_attempt!
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user, message: :two_factor_authenticated, event: :authentication)
end
def handle_two_factor_failure(user, method, message)
user.increment_failed_attempts!
log_failed_two_factor(user, method)
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
flash.now[:alert] = message
prompt_for_two_factor(user)
end
def send_two_factor_otp_attempt_failed_email(user)
user.notification_service.two_factor_otp_attempt_failed(user, request.remote_ip)
end
def log_failed_two_factor(user, method)
# overridden in EE
end
def handle_changed_user(user)
clear_two_factor_attempt!
redirect_to new_user_session_path, alert: _('An error occurred. Please sign in again.')
end
# If user has been updated since we validated the password,
# the password might have changed.
def user_password_changed?(user)
return false unless session[:user_password_hash]
Digest::SHA256.hexdigest(user.encrypted_password) != session[:user_password_hash]
end
end
AuthenticatesWithTwoFactor.prepend_mod_with('AuthenticatesWithTwoFactor')
|