Class: V1::SessionsController

Inherits:
ApplicationController show all
Defined in:
app/controllers/v1/sessions_controller.rb

Constant Summary collapse

REDIRECT_URLS =
%w[signup mhv mhv_verified dslogon dslogon_verified idme idme_verified idme_signup
idme_signup_verified logingov logingov_verified logingov_signup
logingov_signup_verified custom mfa verify slo].freeze
STATSD_SSO_NEW_KEY =
'api.auth.new'
STATSD_SSO_SAMLREQUEST_KEY =
'api.auth.saml_request'
STATSD_SSO_SAMLRESPONSE_KEY =
'api.auth.saml_response'
STATSD_SSO_CALLBACK_KEY =
'api.auth.saml_callback'
STATSD_SSO_CALLBACK_TOTAL_KEY =
'api.auth.login_callback.total'
STATSD_SSO_CALLBACK_FAILED_KEY =
'api.auth.login_callback.failed'
STATSD_LOGIN_NEW_USER_KEY =
'api.auth.new_user'
STATSD_LOGIN_STATUS_SUCCESS =
'api.auth.login.success'
STATSD_LOGIN_STATUS_FAILURE =
'api.auth.login.failure'
STATSD_LOGIN_LATENCY =
'api.auth.latency'
VERSION_TAG =
'version:v1'
FIM_INVALID_MESSAGE_TIMESTAMP =
'invalid_message_timestamp'

Constants inherited from ApplicationController

ApplicationController::VERSION_STATUS

Constants included from SignIn::Authentication

SignIn::Authentication::BEARER_PATTERN

Constants included from ExceptionHandling

ExceptionHandling::SKIP_SENTRY_EXCEPTION_TYPES

Instance Attribute Summary

Attributes inherited from ApplicationController

#current_user

Instance Method Summary collapse

Methods inherited from ApplicationController

#clear_saved_form, #cors_preflight, #pagination_params, #render_job_id, #routing_error, #set_csrf_header

Methods included from Traceable

#set_trace_tags

Methods included from SentryControllerLogging

#set_tags_and_extra_context, #tags_context, #user_context

Methods included from SentryLogging

#log_exception_to_sentry, #log_message_to_sentry, #non_nil_hash?, #normalize_level, #rails_logger, #set_sentry_metadata

Methods included from Instrumentation

#append_info_to_payload

Methods included from SignIn::Authentication

#access_token, #access_token_authenticate, #authenticate_access_token, #bearer_token, #cookie_access_token, #handle_authenticate_error, #load_user, #load_user_object, #scrub_bearer_token, #validate_request_ip

Methods included from Headers

#set_app_info_headers

Methods included from ExceptionHandling

#render_errors, #report_mapped_exception, #report_original_exception, #skip_sentry_exception?, #skip_sentry_exception_types

Methods included from AuthenticationAndSSOConcerns

#clear_session, #extend_session!, #load_user, #log_sso_info, #render_unauthorized, #reset_session, #set_api_cookie!, #set_current_user, #set_session_expiration_header, #set_session_object, #sign_in_service_exp_time, #sign_in_service_session, #sso_cookie_content, #sso_logging_info, #validate_inbound_login_params, #validate_session

Methods included from SignIn::AudienceValidator

#validate_audience!

Instance Method Details

#after_login_actionsObject (private)



410
411
412
413
# File 'app/controllers/v1/sessions_controller.rb', line 410

def 
  Login::AfterLoginActions.new(@current_user, ).perform
  log_persisted_session_and_warnings
end

#authenticateObject (private)



130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/controllers/v1/sessions_controller.rb', line 130

def authenticate
  return unless action_name == 'new'

  if %w[mfa verify].include?(params[:type])
    super
  elsif params[:type] == 'slo'
    # load the session object and current user before attempting to destroy
    load_user
    reset_session
  else
    reset_session
  end
end

#callback_stats(status, saml_response = nil, failure_tag = nil) ⇒ Object (private)



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'app/controllers/v1/sessions_controller.rb', line 336

def callback_stats(status, saml_response = nil, failure_tag = nil)
  tracker = url_service.tracker
  tracker_tags = ["type:#{tracker.payload_attr(:type)}", "client_id:#{tracker.payload_attr(:application)}"]
  case status
  when :success
    StatsD.increment(STATSD_SSO_CALLBACK_KEY,
                     tags: ['status:success', "context:#{saml_response&.authn_context}",
                            VERSION_TAG].concat(tracker_tags))
  when :failure
    tag = failure_tag.to_s.starts_with?('error:') ? failure_tag : "error:#{failure_tag}"
    StatsD.increment(STATSD_SSO_CALLBACK_KEY,
                     tags: ['status:failure', "context:#{saml_response&.authn_context}",
                            VERSION_TAG].concat(tracker_tags))
    StatsD.increment(STATSD_SSO_CALLBACK_FAILED_KEY, tags: [tag, VERSION_TAG])
  when :failed_unknown
    StatsD.increment(STATSD_SSO_CALLBACK_KEY,
                     tags: ['status:failure', 'context:unknown', VERSION_TAG].concat(tracker_tags))
    StatsD.increment(STATSD_SSO_CALLBACK_FAILED_KEY, tags: ['error:unknown', VERSION_TAG])
  when :total
    StatsD.increment(STATSD_SSO_CALLBACK_TOTAL_KEY, tags: [VERSION_TAG])
  end
end

#conditional_log_message_to_sentry(message, level, context, code) ⇒ Object (private)

rubocop:enable Metrics/ParameterLists



387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'app/controllers/v1/sessions_controller.rb', line 387

def conditional_log_message_to_sentry(message, level, context, code)
  # If our error is that we have multiple mhv ids, this is a case where we won't log in the user,
  # but we give them a path to resolve this. So we don't want to throw an error, and we don't want
  # to pollute Sentry with this condition, but we will still log in case we want metrics in
  # Cloudwatch or any other log aggregator. Additionally, if the user has an invalid message timestamp
  # error, this means they have waited too long in the log in page to progress, so it's not really an
  # appropriate Sentry error
  if code == SAML::UserAttributeError::MULTIPLE_MHV_IDS_CODE || invalid_message_timestamp_error?(message)
    Rails.logger.warn("SessionsController version:v1 context:#{context} message:#{message}")
  else
    log_message_to_sentry(message, level, extra_context: context)
  end
end

#create_user_verification(user) ⇒ Object (private)



161
162
163
164
165
166
167
168
169
# File 'app/controllers/v1/sessions_controller.rb', line 161

def create_user_verification(user)
  user_verifier_object = OpenStruct.new({ sign_in: user.identity.,
                                          mhv_correlation_id: user.mhv_correlation_id,
                                          idme_uuid: user.idme_uuid,
                                          edipi: user.identity.edipi,
                                          logingov_uuid: user.logingov_uuid,
                                          icn: user.icn })
  Login::UserVerifier.new(user_verifier_object).perform
end

#delete_sign_in_service_cookiesObject (private)



102
103
104
105
106
107
# File 'app/controllers/v1/sessions_controller.rb', line 102

def 
  cookies.delete(SignIn::Constants::Auth::ACCESS_TOKEN_COOKIE_NAME)
  cookies.delete(SignIn::Constants::Auth::REFRESH_TOKEN_COOKIE_NAME)
  cookies.delete(SignIn::Constants::Auth::ANTI_CSRF_COOKIE_NAME)
  cookies.delete(SignIn::Constants::Auth::INFO_COOKIE_NAME)
end

#handle_callback_error(exc, status, response, level = :error, context = {}, code = SAML::Responses::Base::UNKNOWN_OR_BLANK_ERROR_CODE, tag = nil) ⇒ Object (private)

rubocop:disable Metrics/ParameterLists



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'app/controllers/v1/sessions_controller.rb', line 360

def handle_callback_error(exc, status, response, level = :error, context = {},
                          code = SAML::Responses::Base::UNKNOWN_OR_BLANK_ERROR_CODE, tag = nil)
  # replaces bundled Sentry error message with specific XML messages
  message = if response.normalized_errors.count > 1 && response.status_detail
              response.status_detail
            else
              exc.message
            end
  conditional_log_message_to_sentry(message, level, context, code)
  Rails.logger.info("SessionsController version:v1 saml_callback failure, user_uuid=#{@current_user&.uuid}")

  unless performed?
    redirect_to url_service.(auth: 'fail', code:,
                                               request_id: request.request_id)
  end
  (:failure, exc) unless response.nil?
  callback_stats(status, response, tag)
  PersonalInformationLog.create(
    error_class: exc,
    data: {
      request_id: request.uuid,
      payload: response&.response || params[:SAMLResponse]
    }
  )
end

#html_escaped_relay_stateObject (private)



427
428
429
# File 'app/controllers/v1/sessions_controller.rb', line 427

def html_escaped_relay_state
  JSON.parse(CGI.unescapeHTML(params[:RelayState] || '{}'))
end

#invalid_message_timestamp_error?(message) ⇒ Boolean (private)

Returns:

  • (Boolean)


401
402
403
# File 'app/controllers/v1/sessions_controller.rb', line 401

def invalid_message_timestamp_error?(message)
  message.match(FIM_INVALID_MESSAGE_TIMESTAMP)
end

#log_persisted_session_and_warningsObject (private)



422
423
424
425
# File 'app/controllers/v1/sessions_controller.rb', line 422

def log_persisted_session_and_warnings
  obscure_token = Session.obscure_token(@session_object.token)
  Rails.logger.info("Logged in user with id #{@session_object&.uuid}, token #{obscure_token}")
end

#login_params(type) ⇒ Object (private)

rubocop:disable Metrics/MethodLength



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
# File 'app/controllers/v1/sessions_controller.rb', line 213

def (type)
  raise Common::Exceptions::RoutingError, type unless REDIRECT_URLS.include?(type)

  case type
  when 'mhv'
    url_service.('mhv', 'myhealthevet', AuthnContext::MHV)
  when 'mhv_verified'
    url_service.('mhv_verified', 'myhealthevet', AuthnContext::MHV)
  when 'dslogon'
    url_service.('dslogon', 'dslogon', AuthnContext::DSLOGON)
  when 'dslogon_verified'
    url_service.('dslogon', 'dslogon', AuthnContext::DSLOGON)
  when 'idme'
    url_service.('idme', LOA::IDME_LOA1_VETS, AuthnContext::ID_ME, AuthnContext::MINIMUM)
  when 'idme_verified'
    url_service.('idme', LOA::IDME_LOA3, AuthnContext::ID_ME, AuthnContext::MINIMUM)
  when 'idme_signup'
    url_service.(LOA::IDME_LOA1_VETS)
  when 'idme_signup_verified'
    url_service.(LOA::IDME_LOA3)
  when 'logingov'
    url_service.(
      'logingov',
      [IAL::LOGIN_GOV_IAL1, AAL::LOGIN_GOV_AAL2],
      AuthnContext::LOGIN_GOV,
      AuthnContext::MINIMUM
    )
  when 'logingov_verified'
    url_service.(
      'logingov',
      [IAL::LOGIN_GOV_IAL2, AAL::LOGIN_GOV_AAL2],
      AuthnContext::LOGIN_GOV
    )
  when 'logingov_signup'
    url_service.([IAL::LOGIN_GOV_IAL1, AAL::LOGIN_GOV_AAL2])
  when 'logingov_signup_verified'
    url_service.([IAL::LOGIN_GOV_IAL2, AAL::LOGIN_GOV_AAL2])
  when 'mfa'
    url_service.mfa_url
  when 'verify'
    url_service.verify_url
  when 'custom'
    authn = 
    url_service(false).custom_url authn
  end
end

#login_stats(status, error = nil) ⇒ Object (private)



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'app/controllers/v1/sessions_controller.rb', line 315

def (status, error = nil)
  type = url_service.tracker.payload_attr(:type)
  client_id = url_service.tracker.payload_attr(:application)
  tags = ["type:#{type}", VERSION_TAG, "client_id:#{client_id}"]
  case status
  when :success
    StatsD.increment(STATSD_LOGIN_NEW_USER_KEY, tags: [VERSION_TAG]) if type == 'signup'
    StatsD.increment(STATSD_LOGIN_STATUS_SUCCESS, tags:)
    context = { icn: @current_user.icn, version: 'v1', client_id:, type: }
    Rails.logger.info('LOGIN_STATUS_SUCCESS', context)
    Rails.logger.info("SessionsController version:v1 login complete, user_uuid=#{@current_user.uuid}")
    StatsD.measure(STATSD_LOGIN_LATENCY, url_service.tracker.age, tags:)
  when :failure
    tags_and_error_code = tags << "error:#{error.try(:code) || SAML::Responses::Base::UNKNOWN_OR_BLANK_ERROR_CODE}"
    error_message = error.try(:message) || 'Unknown'
    StatsD.increment(STATSD_LOGIN_STATUS_FAILURE, tags: tags_and_error_code)
    Rails.logger.info("LOGIN_STATUS_FAILURE, tags: #{tags_and_error_code}, message: #{error_message}")
    Rails.logger.info("SessionsController version:v1 login failure, user_uuid=#{@current_user&.uuid}")
  end
end

#metadataObject



95
96
97
98
# File 'app/controllers/v1/sessions_controller.rb', line 95

def 
  meta = OneLogin::RubySaml::Metadata.new
  render xml: meta.generate(saml_settings), content_type: 'application/xml'
end

#mhv_unverified_validation(user) ⇒ Object (private)



171
172
173
174
175
176
177
178
179
# File 'app/controllers/v1/sessions_controller.rb', line 171

def mhv_unverified_validation(user)
  if html_escaped_relay_state['type'] == 'mhv_verified' && user.loa[:current] < LOA::THREE
    mhv_unverified_error = SAML::UserAttributeError::ERRORS[:mhv_unverified_blocked]
    Rails.logger.warn("SessionsController version:v1 #{mhv_unverified_error[:message]}")
    raise SAML::UserAttributeError.new(message: mhv_unverified_error[:message],
                                       code: mhv_unverified_error[:code],
                                       tag: mhv_unverified_error[:tag])
  end
end

#newObject

Collection Action: auth is required for certain types of requests For more details see SAML::SSOeSettingsService and SAML::URLService



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
# File 'app/controllers/v1/sessions_controller.rb', line 35

def new
  type = params[:type]
  client_id = params[:application] || 'vaweb'

  # As a temporary measure while we have the ability to authenticate either through SessionsController
  # or through SignInController, we will delete all SignInController cookies when authenticating with SSOe
  # to prevent undefined authentication behavior
  

  if type == 'slo'
    Rails.logger.info("SessionsController version:v1 LOGOUT of type #{type}", sso_logging_info)
    reset_session
    url = URI.parse(url_service.ssoe_slo_url)

    app_key = if ActiveModel::Type::Boolean.new.cast(params[:agreements_declined])
                Settings.saml_ssoe.tou_decline_logout_app_key
              else
                Settings.saml_ssoe.logout_app_key
              end

    query_strings = { appKey: CGI.escape(app_key), clientId: params[:client_id] }.compact

    url.query = query_strings.to_query

    redirect_to url.to_s
  else
    (type)
  end
  new_stats(type, client_id)
end

#new_stats(type, client_id) ⇒ Object (private)



309
310
311
312
313
# File 'app/controllers/v1/sessions_controller.rb', line 309

def new_stats(type, client_id)
  tags = ["type:#{type}", VERSION_TAG, "client_id:#{client_id}"]
  StatsD.increment(STATSD_SSO_NEW_KEY, tags:)
  Rails.logger.info("SSO_NEW_KEY, tags: #{tags}")
end

#originating_request_idObject (private)



431
432
433
434
435
# File 'app/controllers/v1/sessions_controller.rb', line 431

def originating_request_id
  html_escaped_relay_state['originating_request_id']
rescue
  'UNKNOWN'
end

#raise_saml_error(form) ⇒ Object (private)

Raises:



122
123
124
125
126
127
128
# File 'app/controllers/v1/sessions_controller.rb', line 122

def raise_saml_error(form)
  code = form.error_code
  if code == SAML::Responses::Base::AUTH_TOO_LATE_ERROR_CODE && validate_session
    code = UserSessionForm::ERRORS[:saml_replay_valid_session][:code]
  end
  raise SAML::FormError.new(form, code)
end

#render_login(type) ⇒ Object (private)



181
182
183
184
185
186
187
188
189
190
191
# File 'app/controllers/v1/sessions_controller.rb', line 181

def (type)
  , post_params = (type)
  renderer = ActionController::Base.renderer
  renderer.controller.prepend_view_path(Rails.root.join('lib', 'saml', 'templates'))
  result = renderer.render template: 'sso_post_form',
                           locals: { url: , params: post_params },
                           format: :html
  render body: result, content_type: 'text/html'
  set_sso_saml_cookie!
  saml_request_stats
end

#saml_callbackObject



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'app/controllers/v1/sessions_controller.rb', line 76

def saml_callback
  set_sentry_context_for_callback if html_escaped_relay_state['type'] == 'mfa'
  saml_response = SAML::Responses::Login.new(params[:SAMLResponse], settings: saml_settings)
  saml_response_stats(saml_response)
  raise_saml_error(saml_response) unless saml_response.valid?
  (saml_response)
  callback_stats(:success, saml_response)
  Rails.logger.info("SessionsController version:v1 saml_callback complete, user_uuid=#{@current_user&.uuid}")
rescue SAML::SAMLError => e
  handle_callback_error(e, :failure, saml_response, e.level, e.context, e.code, e.tag)
rescue => e
  # the saml_response variable may or may not be defined depending on
  # where the exception was raised
  resp = defined?(saml_response) && saml_response
  handle_callback_error(e, :failed_unknown, resp)
ensure
  callback_stats(:total)
end


203
204
205
206
207
208
209
210
# File 'app/controllers/v1/sessions_controller.rb', line 203

def saml_cookie_content
  {
    'timestamp' => Time.now.iso8601,
    'transaction_id' => url_service.tracker&.payload_attr(:transaction_id),
    'saml_request_id' => url_service.tracker&.uuid,
    'saml_request_query_params' => url_service.query_params
  }
end

#saml_request_statsObject (private)

rubocop:enable Metrics/MethodLength



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'app/controllers/v1/sessions_controller.rb', line 261

def saml_request_stats
  tracker = url_service.tracker
  authn_context = tracker&.payload_attr(:authn_context)
  values = {
    'id' => tracker&.uuid,
    'authn' => authn_context,
    'type' => tracker&.payload_attr(:type),
    'transaction_id' => tracker&.payload_attr(:transaction_id)
  }
  Rails.logger.info("SSOe: SAML Request => #{values}")
  normalized_authn = authn_context.is_a?(Array) ? authn_context.join('_').prepend('_') : authn_context
  StatsD.increment(STATSD_SSO_SAMLREQUEST_KEY,
                   tags: ["type:#{tracker&.payload_attr(:type)}",
                          "context:#{normalized_authn}",
                          "client_id:#{tracker&.payload_attr(:application)}",
                          VERSION_TAG])
end

#saml_response_stats(saml_response) ⇒ Object (private)



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'app/controllers/v1/sessions_controller.rb', line 279

def saml_response_stats(saml_response)
  uuid = saml_response.in_response_to
  tracker = SAMLRequestTracker.find(uuid)
  values = {
    'id' => uuid,
    'authn' => saml_response.authn_context,
    'type' => tracker&.payload_attr(:type),
    'transaction_id' => tracker&.payload_attr(:transaction_id),
    'authentication_time' => tracker&.created_at ? Time.zone.now.to_i - tracker.created_at : 'unknown'
  }
  Rails.logger.info("SSOe: SAML Response => #{values}")
  StatsD.increment(STATSD_SSO_SAMLRESPONSE_KEY,
                   tags: ["type:#{tracker&.payload_attr(:type)}",
                          "client_id:#{tracker&.payload_attr(:application)}",
                          "context:#{saml_response.authn_context}",
                          VERSION_TAG])
end

#saml_settings(force_authn: true) ⇒ Object (private)



118
119
120
# File 'app/controllers/v1/sessions_controller.rb', line 118

def saml_settings(force_authn: true)
  SAML::SSOeSettingsService.saml_settings(force_authn:)
end

#set_cookiesObject (private)



405
406
407
408
# File 'app/controllers/v1/sessions_controller.rb', line 405

def set_cookies
  Rails.logger.info('SSO: LOGIN', sso_logging_info)
  set_api_cookie!
end

#set_sentry_context_for_callbackObject (private)



109
110
111
112
113
114
115
116
# File 'app/controllers/v1/sessions_controller.rb', line 109

def set_sentry_context_for_callback
  temp_session_object = Session.find(session[:token])
  temp_current_user = User.find(temp_session_object.uuid) if temp_session_object&.uuid
  Sentry.set_extras(
    current_user_uuid: temp_current_user.try(:uuid),
    current_user_icn: temp_current_user.try(:mhv_icn)
  )
end

#set_sso_saml_cookie!Object (private)



193
194
195
196
197
198
199
200
201
# File 'app/controllers/v1/sessions_controller.rb', line 193

def set_sso_saml_cookie!
  cookies[Settings.ssoe_eauth_cookie.name] = {
    value: saml_cookie_content.to_json,
    expires: nil,
    secure: Settings.ssoe_eauth_cookie.secure,
    httponly: true,
    domain: Settings.ssoe_eauth_cookie.domain
  }
end

#skip_mhv_account_creationObject (private)



415
416
417
418
419
420
# File 'app/controllers/v1/sessions_controller.rb', line 415

def 
   = url_service.tracker.payload_attr(:application) == SAML::User::MHV_ORIGINAL_CSID
   = url_service.tracker.payload_attr(:type) == 'custom'

   || 
end

#ssoe_slo_callbackObject



66
67
68
69
70
71
72
73
74
# File 'app/controllers/v1/sessions_controller.rb', line 66

def ssoe_slo_callback
  Rails.logger.info("SessionsController version:v1 ssoe_slo_callback, user_uuid=#{@current_user&.uuid}")

  if ActiveModel::Type::Boolean.new.cast(params[:agreements_declined])
    redirect_to url_service.tou_declined_logout_redirect_url
  else
    redirect_to url_service.logout_redirect_url
  end
end

#url_service(force_authn = true) ⇒ Object (private)



437
438
439
440
441
442
443
# File 'app/controllers/v1/sessions_controller.rb', line 437

def url_service(force_authn = true)
  @url_service ||= SAML::PostURLService.new(saml_settings(force_authn:),
                                            session: @session_object,
                                            user: current_user,
                                            params:,
                                            loa3_context: LOA::IDME_LOA3)
end

#user_login(saml_response) ⇒ Object (private)



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/controllers/v1/sessions_controller.rb', line 144

def (saml_response)
  user_session_form = UserSessionForm.new(saml_response)
  raise_saml_error(user_session_form) unless user_session_form.valid?
  mhv_unverified_validation(user_session_form.user)
  create_user_verification(user_session_form.user)
  @current_user, @session_object = user_session_form.persist
  set_cookies
  

  if @current_user.needs_accepted_terms_of_use
    redirect_to url_service.terms_of_use_redirect_url
  else
    redirect_to url_service.
  end
  (:success)
end

#user_logout(saml_response) ⇒ Object (private)



297
298
299
300
301
302
303
304
305
306
307
# File 'app/controllers/v1/sessions_controller.rb', line 297

def user_logout(saml_response)
  logout_request = SingleLogoutRequest.find(saml_response&.in_response_to)
  if logout_request.present?
    logout_request.destroy
    Rails.logger.info("SLO callback response to '#{saml_response&.in_response_to}' for originating_request_id "\
                      "'#{originating_request_id}'")
  else
    Rails.logger.info('SLO callback response could not resolve logout request for originating_request_id '\
                      "'#{originating_request_id}'")
  end
end