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

authenticates_with_two_factor.rb « concerns « controllers « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 41a3ee3e1c8ab5055aa91156e4b1ac01bd4d9841 (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
# 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)

    add_gon_variables
    setup_webauthn_authentication(user)

    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]
      authenticate_with_two_factor_via_webauthn(user)
    elsif user && user.valid_password?(user_params[:password])
      prompt_for_two_factor(user)
    end
  rescue ActiveRecord::RecordInvalid => e
    # We expect User to always be valid.
    # Otherwise, raise internal server error instead of unprocessable entity to improve observability/alerting
    if e.record.is_a?(User)
      raise e.message
    else
      raise e
    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

  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

  # rubocop: disable CodeReuse/ActiveRecord
  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[:challenge] = get_options.challenge
      gon.push(webauthn: { options: Gitlab::Json.dump(get_options) })
    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')