Class: SAML::URLService

Inherits:
Object
  • Object
show all
Defined in:
lib/saml/url_service.rb

Overview

This class is responsible for providing the URLs for the various SSO and SLO endpoints

Direct Known Subclasses

PostURLService

Constant Summary collapse

VIRTUAL_HOST_MAPPINGS =
{
  'https://api.vets.gov' => { base_redirect: 'https://www.vets.gov' },
  'https://staging-api.vets.gov' => { base_redirect: 'https://staging.vets.gov' },
  'https://dev-api.vets.gov' => { base_redirect: 'https://dev.vets.gov' },
  'https://api.va.gov' => { base_redirect: 'https://www.va.gov' },
  'https://staging-api.va.gov' => { base_redirect: 'https://staging.va.gov' },
  'https://dev-api.va.gov' => { base_redirect: 'https://dev.va.gov' },
  'http://localhost:3000' => { base_redirect: "http://#{localhost_redirect}:3001" },
  'http://127.0.0.1:3000' => { base_redirect: "http://#{localhost_ip_redirect}:3001" }
}.freeze
LOGIN_REDIRECT_PARTIAL =
'/auth/login/callback'
LOGOUT_REDIRECT_PARTIAL =
'/logout/'
BROKER_CODE =
'iam'
WEB_CLIENT_ID =
'web'
MOBILE_CLIENT_ID =
'mobile'
UNIFIED_SIGN_IN_CLIENTS =
%w[vaweb mhv myvahealth ebenefits vamobile vaoccmobile].freeze
TERMS_OF_USE_DECLINED_PATH =
'/terms-of-use/declined'
SKIP_MHV_ACCOUNT_CREATION_CLIENTS =
%w[mhv custom].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(saml_settings, session: nil, user: nil, params: {}, loa3_context: LOA::IDME_LOA3_VETS) ⇒ URLService

Returns a new instance of URLService.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/saml/url_service.rb', line 33

def initialize(saml_settings, session: nil, user: nil, params: {}, loa3_context: LOA::IDME_LOA3_VETS)
  unless %w[new saml_callback saml_logout_callback ssoe_slo_callback].include?(params[:action])
    raise Common::Exceptions::RoutingError, params[:path]
  end

  if session.present?
    @session = session
    @user = user
    @authn_context = user&.authn_context
  end

  @saml_settings = saml_settings
  @loa3_context = loa3_context

  if (params[:action] == 'saml_callback') && params[:RelayState].present?
    @type = JSON.parse(CGI.unescapeHTML(params[:RelayState]))['type']
  end
  @query_params = {}
  @tracker = initialize_tracker(params)

  Sentry.set_extras(params:)
  Sentry.set_user(session:, user:)
end

Instance Attribute Details

#authn_contextObject (readonly)

Returns the value of attribute authn_context.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def authn_context
  @authn_context
end

#query_paramsObject (readonly)

Returns the value of attribute query_params.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def query_params
  @query_params
end

#saml_settingsObject (readonly)

Returns the value of attribute saml_settings.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def saml_settings
  @saml_settings
end

#sessionObject (readonly)

Returns the value of attribute session.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def session
  @session
end

#trackerObject (readonly)

Returns the value of attribute tracker.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def tracker
  @tracker
end

#typeObject (readonly)

Returns the value of attribute type.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def type
  @type
end

#userObject (readonly)

Returns the value of attribute user.



31
32
33
# File 'lib/saml/url_service.rb', line 31

def user
  @user
end

Instance Method Details

#add_query(url, params) ⇒ Object (private)



216
217
218
219
220
221
222
223
224
# File 'lib/saml/url_service.rb', line 216

def add_query(url, params)
  if params.any?
    uri = URI.parse(url)
    uri.query = Rack::Utils.parse_nested_query(uri.query).merge(params).to_query
    uri.to_s
  else
    url
  end
end

#base_redirect_urlObject

REDIRECT_URLS



58
59
60
# File 'lib/saml/url_service.rb', line 58

def base_redirect_url
  VIRTUAL_HOST_MAPPINGS[current_host][:base_redirect]
end

#build_authn_context(assurance_level_url, identity_provider) ⇒ Object (private)



191
192
193
194
# File 'lib/saml/url_service.rb', line 191

def build_authn_context(assurance_level_url, identity_provider)
  assurance_level_url = [assurance_level_url] unless assurance_level_url.is_a?(Array)
  assurance_level_url.push(identity_provider)
end

#build_sso_url(link_authn_context, authn_con_compare = AuthnContext::EXACT) ⇒ Object (private)

Builds the urls to trigger various SSO policies: mhv, dslogon, idme, mfa, or verify flows. link_authn_context is the new proposed authn_context



181
182
183
184
185
186
187
188
189
# File 'lib/saml/url_service.rb', line 181

def build_sso_url(link_authn_context, authn_con_compare = AuthnContext::EXACT)
  @query_params[:RelayState] = relay_state_params
  new_url_settings = url_settings
  new_url_settings.authn_context = link_authn_context
  new_url_settings.authn_context_comparison = authn_con_compare
  saml_auth_request = OneLogin::RubySaml::Authrequest.new
  save_saml_request_tracker(saml_auth_request.uuid, link_authn_context)
  saml_auth_request.create(new_url_settings, query_params)
end

#callback_verify_urlObject



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/saml/url_service.rb', line 132

def callback_verify_url
  link_authn_context =
    case type
    when 'logingov'
      build_authn_context([IAL::LOGIN_GOV_IAL2, AAL::LOGIN_GOV_AAL2], AuthnContext::LOGIN_GOV)
    when 'mhv', 'mhv_verified'
      build_authn_context('myhealthevet_loa3', AuthnContext::MHV)
    when 'dslogon'
      build_authn_context('dslogon_loa3', AuthnContext::DSLOGON)
    end

  build_sso_url(link_authn_context)
end

#current_hostObject (private)



205
206
207
208
# File 'lib/saml/url_service.rb', line 205

def current_host
  uri = URI.parse(saml_settings.assertion_consumer_service_url)
  URI.join(uri, '/').to_s.chop
end

#custom_url(authn) ⇒ Object



104
105
106
107
# File 'lib/saml/url_service.rb', line 104

def custom_url(authn)
  @type = 'custom'
  build_sso_url(authn)
end

#idme_signup_url(authn_context) ⇒ Object



91
92
93
94
95
# File 'lib/saml/url_service.rb', line 91

def (authn_context)
  @type = 'signup'
  @query_params[:op] = 'signup'
  build_sso_url(build_authn_context(authn_context, AuthnContext::ID_ME))
end

#initialize_tracker(params) ⇒ Object (private)

Initialize a new SAMLRequestTracker, if a valid previous SAML UUID is given, copy over the payload and created_at timestamp. This is useful for a user that has to go through the upleveling process.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/saml/url_service.rb', line 236

def initialize_tracker(params)
  uuid = previous_saml_uuid(params)
  previous = uuid && SAMLRequestTracker.find(uuid)
  type = previous&.payload_attr(:type) || params[:type]
  transaction_id = previous&.payload_attr(:transaction_id) || SecureRandom.uuid
  redirect = previous&.payload_attr(:redirect) || params[:redirect]
  application = previous&.payload_attr(:application) || params[:application] || 'vaweb'
   = previous&.payload_attr(:post_login) || params[:postLogin]

  # if created_at is set to nil (meaning no previous tracker to use), it
  # will be initialized to the current time when it is saved
  SAMLRequestTracker.new(
    payload: { type:,
               transaction_id:,
               redirect:,
               application:,
               post_login: }.compact,

    created_at: previous&.created_at
  )
end

#login_redirect_url(auth: 'success', code: nil) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/saml/url_service.rb', line 62

def (auth: 'success', code: nil)
  return verify_url if auth == 'success' && user.loa[:current] < user.loa[:highest]

  # if the original auth request was an inbound ssoe autologin (type custom)
  # and authentication failed, set 'force-needed' so the FE can silently fail
  # authentication and NOT show the user an error page
  auth = 'force-needed' if auth != 'success' && @tracker&.payload_attr(:type) == 'custom'

  @query_params[:type] = type if type
  @query_params[:auth] = auth if auth != 'success'
  @query_params[:code] = code if code

  if Settings.saml_ssoe.relay.present?
    add_query(Settings.saml_ssoe.relay, query_params)
  else
    add_query("#{base_redirect_url}#{LOGIN_REDIRECT_PARTIAL}", query_params)
  end
end

#login_url(type, authn_context, identity_provider, authn_con_compare = AuthnContext::EXACT) ⇒ Object

SIGN ON URLS



86
87
88
89
# File 'lib/saml/url_service.rb', line 86

def (type, authn_context, identity_provider, authn_con_compare = AuthnContext::EXACT)
  @type = type
  build_sso_url(build_authn_context(authn_context, identity_provider), authn_con_compare)
end

#logingov_signup_url(authn_context) ⇒ Object



97
98
99
100
101
102
# File 'lib/saml/url_service.rb', line 97

def (authn_context)
  @type = 'signup'
  build_sso_url(
    build_authn_context(authn_context, AuthnContext::LOGIN_GOV)
  )
end

#logout_redirect_urlObject



81
82
83
# File 'lib/saml/url_service.rb', line 81

def logout_redirect_url
  "#{base_redirect_url}#{LOGOUT_REDIRECT_PARTIAL}"
end

#mfa_urlObject



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/saml/url_service.rb', line 146

def mfa_url
  @type = 'mfa'
  link_authn_context =
    case authn_context
    when LOA::IDME_LOA1_VETS, LOA::IDME_LOA3_VETS, LOA::IDME_LOA3
      build_authn_context('multifactor', AuthnContext::ID_ME)
    when 'myhealthevet', 'myhealthevet_loa3'
      build_authn_context('myhealthevet_multifactor', AuthnContext::MHV)
    when 'dslogon', 'dslogon_loa3'
      build_authn_context('dslogon_multifactor', AuthnContext::DSLOGON)
    when SAML::UserAttributes::SSOe::INBOUND_AUTHN_CONTEXT
      "#{@user.identity.[:service_name]}_multifactor"
    end
  build_sso_url(link_authn_context)
end

#previous_saml_uuid(params) ⇒ Object (private)



226
227
228
229
230
231
# File 'lib/saml/url_service.rb', line 226

def previous_saml_uuid(params)
  if params[:action] == 'saml_callback'
    resp = SAML::Responses::Login.new(params[:SAMLResponse] || '', settings: @saml_settings)
    resp&.in_response_to
  end
end

#relay_state_paramsObject (private)



196
197
198
199
200
201
202
203
# File 'lib/saml/url_service.rb', line 196

def relay_state_params
  rs_params = {
    originating_request_id: RequestStore.store['request_id'],
    type:
  }
  rs_params[:review_instance_slug] = Settings.review_instance_slug unless Settings.review_instance_slug.nil?
  rs_params.to_json
end

#save_saml_request_tracker(uuid, authn_context) ⇒ Object (private)



258
259
260
261
262
# File 'lib/saml/url_service.rb', line 258

def save_saml_request_tracker(uuid, authn_context)
  @tracker.uuid = uuid
  @tracker.payload[:authn_context] = authn_context
  @tracker.save
end

#slo_urlObject

SIGN OFF URLS



163
164
165
166
167
168
169
170
# File 'lib/saml/url_service.rb', line 163

def slo_url
  @type = 'slo'
  logout_request = OneLogin::RubySaml::Logoutrequest.new
  # cache the request for session.token lookup when we receive the response
  SingleLogoutRequest.create(uuid: logout_request.uuid, token: session.token)
  Rails.logger.info "New SP SLO having logout_request '#{logout_request.uuid}' for userid '#{session.uuid}'"
  logout_request.create(url_settings, RelayState: relay_state_params)
end

#ssoe_slo_urlObject

logout URL for SSOe



173
174
175
# File 'lib/saml/url_service.rb', line 173

def ssoe_slo_url
  Settings.saml_ssoe.logout_url
end

#url_settingsObject (private)



210
211
212
213
214
# File 'lib/saml/url_service.rb', line 210

def url_settings
  url_settings = saml_settings.dup
  url_settings.name_identifier_value = session&.uuid
  url_settings
end

#verify_urlObject



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/saml/url_service.rb', line 109

def verify_url
  # For verification from a login callback, type should be the initial login policy.
  # In that case, it will have been set to the type from RelayState.
  @type ||= 'verify'
  return callback_verify_url if %w[logingov mhv dslogon].include?(type)

  link_authn_context =
    case authn_context
    when LOA::IDME_LOA1_VETS, 'multifactor'
      build_authn_context(@loa3_context, AuthnContext::ID_ME)
    when IAL::LOGIN_GOV_IAL1
      build_authn_context([IAL::LOGIN_GOV_IAL2, AAL::LOGIN_GOV_AAL2], AuthnContext::LOGIN_GOV)
    when 'myhealthevet', 'myhealthevet_multifactor'
      build_authn_context('myhealthevet_loa3', AuthnContext::MHV)
    when 'dslogon', 'dslogon_multifactor'
      build_authn_context('dslogon_loa3', AuthnContext::DSLOGON)
    when SAML::UserAttributes::SSOe::INBOUND_AUTHN_CONTEXT
      "#{@user.identity.[:service_name]}_loa3"
    end

  build_sso_url(link_authn_context, AuthnContext::EXACT)
end