summaryrefslogtreecommitdiff
path: root/app/controllers/omniauth_callbacks_controller.rb
blob: 8650b6cbc6fabd913afe4a5820ccca680554d8d0 (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
# frozen_string_literal: true

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
  include AuthenticatesWithTwoFactorForAdminMode
  include Devise::Controllers::Rememberable
  include AuthHelper
  include InitializesCurrentUserMode
  include KnownSignIn
  include AcceptsPendingInvitations

  after_action :verify_known_sign_in

  protect_from_forgery except: [:cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true

  feature_category :authentication_and_authorization

  def handle_omniauth
    omniauth_flow(Gitlab::Auth::OAuth)
  end

  AuthHelper.providers_for_base_controller.each do |provider|
    alias_method provider, :handle_omniauth
  end

  # Extend the standard implementation to also increment
  # the number of failed sign in attempts
  def failure
    if params[:username].present? && AuthHelper.form_based_provider?(failed_strategy.name)
      user = User.find_by_login(params[:username])

      user&.increment_failed_attempts!
      log_failed_login(params[:username], failed_strategy.name)
    end

    super
  end

  # Extend the standard message generation to accept our custom exception
  def failure_message
    exception = request.env["omniauth.error"]
    error = exception.error_reason if exception.respond_to?(:error_reason)
    error ||= exception.error        if exception.respond_to?(:error)
    error ||= exception.message      if exception.respond_to?(:message)
    error ||= request.env["omniauth.error.type"].to_s

    error.to_s.humanize if error
  end

  def saml
    omniauth_flow(Gitlab::Auth::Saml)
  rescue Gitlab::Auth::Saml::IdentityLinker::UnverifiedRequest
    redirect_unverified_saml_initiation
  end

  def cas3
    ticket = params['ticket']
    if ticket
      handle_service_ticket oauth['provider'], ticket
    end

    handle_omniauth
  end

  def authentiq
    if params['sid']
      handle_service_ticket oauth['provider'], params['sid']
    end

    handle_omniauth
  end

  def auth0
    if oauth['uid'].blank?
      fail_auth0_login
    else
      handle_omniauth
    end
  end

  def salesforce
    if oauth.dig('extra', 'email_verified')
      handle_omniauth
    else
      fail_salesforce_login
    end
  end

  def atlassian_oauth2
    omniauth_flow(Gitlab::Auth::Atlassian)
  end

  private

  def log_failed_login(user, provider)
    # overridden in EE
  end

  def after_omniauth_failure_path_for(scope)
    if Gitlab::CurrentSettings.admin_mode
      return new_admin_session_path if current_user_mode.admin_mode_requested?
    end

    super
  end

  def omniauth_flow(auth_module, identity_linker: nil)
    if fragment = request.env.dig('omniauth.params', 'redirect_fragment').presence
      store_redirect_fragment(fragment)
    end

    if current_user
      return render_403 unless link_provider_allowed?(oauth['provider'])

      log_audit_event(current_user, with: oauth['provider'])

      if Gitlab::CurrentSettings.admin_mode
        return admin_mode_flow(auth_module::User) if current_user_mode.admin_mode_requested?
      end

      identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
      link_identity(identity_linker)
      set_remember_me(current_user)

      if identity_linker.changed?
        redirect_identity_linked
      elsif identity_linker.failed?
        redirect_identity_link_failed(identity_linker.error_message)
      else
        redirect_identity_exists
      end
    else
      sign_in_user_flow(auth_module::User)
    end
  end

  def link_identity(identity_linker)
    identity_linker.link
  end

  def redirect_identity_exists
    redirect_to after_sign_in_path_for(current_user)
  end

  def redirect_identity_link_failed(error_message)
    redirect_to profile_account_path, notice: _("Authentication failed: %{error_message}") % { error_message: error_message }
  end

  def redirect_identity_linked
    redirect_to profile_account_path, notice: _('Authentication method updated')
  end

  def handle_service_ticket(provider, ticket)
    Gitlab::Auth::OAuth::Session.create provider, ticket
    session[:service_tickets] ||= {}
    session[:service_tickets][provider] = ticket
  end

  def build_auth_user(auth_user_class)
    auth_user_class.new(oauth)
  end

  def sign_in_user_flow(auth_user_class)
    auth_user = build_auth_user(auth_user_class)
    new_user = auth_user.new?
    user = auth_user.find_and_update!

    if auth_user.valid_sign_in?
      # In this case the `#current_user` would not be set. So we can't fetch it
      # from that in `#context_user`. Pushing it manually here makes the information
      # available in the logs for this request.
      Gitlab::ApplicationContext.push(user: user)
      log_audit_event(user, with: oauth['provider'])
      Gitlab::Tracking.event(self.class.name, "#{oauth['provider']}_sso", user: user) if new_user

      set_remember_me(user)

      if user.two_factor_enabled? && !auth_user.bypass_two_factor?
        prompt_for_two_factor(user)
      else
        if user.deactivated?
          user.activate
          flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
        end

        accept_pending_invitations(user: user) if new_user
        persist_accepted_terms_if_required(user) if new_user

        store_after_sign_up_path_for_user if intent_to_register?
        sign_in_and_redirect(user, event: :authentication)
      end
    else
      fail_login(user)
    end
  rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
    handle_disabled_provider
  rescue Gitlab::Auth::OAuth::User::SignupDisabledError
    handle_signup_error
  end

  def handle_signup_error
    label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
    message = [_("Signing in using your %{label} account without a pre-existing GitLab account is not allowed.") % { label: label }]

    if Gitlab::CurrentSettings.allow_signup?
      message << _("Create a GitLab account first, and then connect it to your %{label} account.") % { label: label }
    end

    flash[:alert] = message.join(' ')
    redirect_to new_user_session_path
  end

  def oauth
    @oauth ||= request.env['omniauth.auth']
  end

  def fail_login(user)
    log_failed_login(user.username, oauth['provider'])

    @provider = Gitlab::Auth::OAuth::Provider.label_for(params[:action])
    @error = user.errors.full_messages.to_sentence

    render 'errors/omniauth_error', layout: "oauth_error", status: :unprocessable_entity
  end

  def fail_auth0_login
    fail_login_with_message(_('Wrong extern UID provided. Make sure Auth0 is configured correctly.'))
  end

  def fail_salesforce_login
    fail_login_with_message(_('Email not verified. Please verify your email in Salesforce.'))
  end

  def fail_login_with_message(message)
    flash[:alert] = message

    redirect_to new_user_session_path
  end

  def redirect_unverified_saml_initiation
    redirect_to profile_account_path, notice: _('Request to link SAML account must be authorized')
  end

  def handle_disabled_provider
    label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
    flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label }

    redirect_to new_user_session_path
  end

  def log_audit_event(user, options = {})
    AuditEventService.new(user, user, options)
      .for_authentication.security_event
  end

  def set_remember_me(user)
    return unless remember_me?

    if user.two_factor_enabled?
      params[:remember_me] = '1'
    else
      remember_me(user)
    end
  end

  def remember_me?
    request_params = request.env['omniauth.params']
    (request_params['remember_me'] == '1') if request_params.present?
  end

  def intent_to_register?
    request_params = request.env['omniauth.params']
    (request_params['intent'] == 'register') if request_params.present?
  end

  def store_redirect_fragment(redirect_fragment)
    key = stored_location_key_for(:user)
    location = session[key]
    if uri = parse_uri(location)
      uri.fragment = redirect_fragment
      store_location_for(:user, uri.to_s)
    end
  end

  def admin_mode_flow(auth_user_class)
    auth_user = build_auth_user(auth_user_class)

    return fail_admin_mode_invalid_credentials unless omniauth_identity_matches_current_user?

    if current_user.two_factor_enabled? && !auth_user.bypass_two_factor?
      admin_mode_prompt_for_two_factor(current_user)
    else
      # Can only reach here if the omniauth identity matches current user
      # and current_user is an admin that requested admin mode
      current_user_mode.enable_admin_mode!(skip_password_validation: true)

      redirect_to stored_location_for(:redirect) || admin_root_path, notice: _('Admin mode enabled')
    end
  end

  def omniauth_identity_matches_current_user?
    current_user.matches_identity?(oauth['provider'], oauth['uid'])
  end

  def fail_admin_mode_invalid_credentials
    redirect_to new_admin_session_path, alert: _('Invalid login or password')
  end

  def persist_accepted_terms_if_required(user)
    return unless Feature.enabled?(:update_oauth_registration_flow)
    return unless user.persisted?
    return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?

    terms = ApplicationSetting::Term.latest
    Users::RespondToTermsService.new(user, terms).execute(accepted: true)
  end

  def store_after_sign_up_path_for_user
    store_location_for(:user, users_sign_up_welcome_path)
  end
end

OmniauthCallbacksController.prepend_mod_with('OmniauthCallbacksController')