Class: SignIn::Logingov::Service

Inherits:
Common::Client::Base show all
Includes:
PublicJwks
Defined in:
lib/sign_in/logingov/service.rb

Constant Summary collapse

DEFAULT_SCOPES =
[
  PROFILE_SCOPE = 'profile',
  VERIFIED_AT_SCOPE = 'profile:verified_at',
  ADDRESS_SCOPE = 'address',
  EMAIL_SCOPE = 'email',
  OPENID_SCOPE = 'openid',
  SSN_SCOPE = 'social_security_number'
].freeze
OPTIONAL_SCOPES =
[ALL_EMAILS_SCOPE = 'all_emails'].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from PublicJwks

#jwks_loader, #parse_public_jwks, #public_jwks

Methods inherited from Common::Client::Base

#config, configuration, #connection, #delete, #get, #perform, #post, #put, #raise_backend_exception, #raise_not_authenticated, #request, #sanitize_headers!, #service_name

Methods included from SentryLogging

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

Constructor Details

#initialize(optional_scopes: []) ⇒ Service

Returns a new instance of Service.



27
28
29
30
# File 'lib/sign_in/logingov/service.rb', line 27

def initialize(optional_scopes: [])
  @optional_scopes = valid_optional_scopes(optional_scopes)
  super()
end

Instance Attribute Details

#optional_scopesObject (readonly)

Returns the value of attribute optional_scopes.



25
26
27
# File 'lib/sign_in/logingov/service.rb', line 25

def optional_scopes
  @optional_scopes
end

Instance Method Details

#auth_params(acr, state, scope) ⇒ Object (private)



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/sign_in/logingov/service.rb', line 94

def auth_params(acr, state, scope)
  {
    acr_values: acr,
    client_id: config.client_id,
    nonce: random_seed,
    prompt: config.prompt,
    redirect_uri: config.redirect_uri,
    response_type: config.response_type,
    scope:,
    state:
  }
end

#auth_urlObject (private)



164
165
166
# File 'lib/sign_in/logingov/service.rb', line 164

def auth_url
  "#{config.base_path}/#{config.auth_path}"
end

#client_assertion_jwtObject (private)



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/sign_in/logingov/service.rb', line 204

def client_assertion_jwt
  jwt_payload = {
    iss: config.client_id,
    sub: config.client_id,
    aud: token_url,
    jti: SecureRandom.hex,
    nonce: random_seed,
    exp: Time.now.to_i + config.client_assertion_expiration_seconds
  }
  JWT.encode(jwt_payload, config.ssl_key, 'RS256')
end

#encode_logout_redirect(logout_redirect_uri) ⇒ Object (private)



193
194
195
# File 'lib/sign_in/logingov/service.rb', line 193

def encode_logout_redirect(logout_redirect_uri)
  Base64.encode64(logout_state_payload(logout_redirect_uri).to_json)
end

#get_authn_context(current_ial) ⇒ Object (private)



160
161
162
# File 'lib/sign_in/logingov/service.rb', line 160

def get_authn_context(current_ial)
  current_ial == Constants::Auth::IAL_TWO ? Constants::Auth::LOGIN_GOV_IAL2 : Constants::Auth::LOGIN_GOV_IAL1
end

#jwt_decode(encoded_jwt) ⇒ Object (private)



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/sign_in/logingov/service.rb', line 142

def jwt_decode(encoded_jwt)
  verify_expiration = true
  JWT.decode(
    encoded_jwt,
    nil,
    verify_expiration,
    { verify_expiration:, algorithm: config.jwt_decode_algorithm, jwks: method(:jwks_loader) }
  ).first
rescue JWT::JWKError
  raise Errors::PublicJWKError, '[SignIn][Logingov][Service] Public JWK is malformed'
rescue JWT::VerificationError
  raise Errors::JWTVerificationError, '[SignIn][Logingov][Service] JWT body does not match signature'
rescue JWT::ExpiredSignature
  raise Errors::JWTExpiredError, '[SignIn][Logingov][Service] JWT has expired'
rescue JWT::DecodeError
  raise Errors::JWTDecodeError, '[SignIn][Logingov][Service] JWT is malformed'
end

#log_credential(credential) ⇒ Object (private)



125
126
127
# File 'lib/sign_in/logingov/service.rb', line 125

def log_credential(credential)
  MockedAuthentication::Mockdata::Writer.save_credential(credential:, credential_type: 'logingov')
end

#logout_state_payload(logout_redirect_uri) ⇒ Object (private)



197
198
199
200
201
202
# File 'lib/sign_in/logingov/service.rb', line 197

def logout_state_payload(logout_redirect_uri)
  {
    logout_redirect: logout_redirect_uri,
    seed: random_seed
  }
end

#normalize_address(address) ⇒ Object (private)



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/sign_in/logingov/service.rb', line 107

def normalize_address(address)
  return unless address

  street_array = address[:street_address].split("\n")
  {
    street: street_array[0],
    street2: street_array[1],
    postal_code: address[:postal_code],
    state: address[:region],
    city: address[:locality],
    country: united_states_country_code
  }
end

#normalized_attributes(user_info, credential_level) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/sign_in/logingov/service.rb', line 73

def normalized_attributes(, credential_level)
  {
    logingov_uuid: .sub,
    current_ial: credential_level.current_ial,
    max_ial: credential_level.max_ial,
    ssn: .social_security_number&.tr('-', ''),
    birth_date: .birthdate,
    first_name: .given_name,
    last_name: .family_name,
    address: normalize_address(.address),
    csp_email: .email,
    all_csp_emails: .all_emails,
    multifactor: true,
    service_name: config.service_name,
    authn_context: get_authn_context(credential_level.current_ial),
    auto_uplevel: credential_level.auto_uplevel
  }
end

#parse_token_response(response_body) ⇒ Object (private)



136
137
138
139
140
# File 'lib/sign_in/logingov/service.rb', line 136

def parse_token_response(response_body)
  access_token = response_body[:access_token]
  logingov_acr = jwt_decode(response_body[:id_token])['acr']
  { access_token:, logingov_acr: }
end

#raise_client_error(client_error, function_name) ⇒ Object (private)

Raises:

  • (client_error)


129
130
131
132
133
134
# File 'lib/sign_in/logingov/service.rb', line 129

def raise_client_error(client_error, function_name)
  status = client_error.status
  description = client_error.body && client_error.body[:error]
  raise client_error, "[SignIn][Logingov][Service] Cannot perform #{function_name} request, " \
                      "status: #{status}, description: #{description}"
end

#random_seedObject (private)



216
217
218
# File 'lib/sign_in/logingov/service.rb', line 216

def random_seed
  @random_seed ||= SecureRandom.hex
end

#render_auth(state: SecureRandom.hex, acr: Constants::Auth::LOGIN_GOV_IAL1, operation: Constants::Auth::AUTHORIZE) ⇒ Object



32
33
34
35
36
37
38
39
40
41
# File 'lib/sign_in/logingov/service.rb', line 32

def render_auth(state: SecureRandom.hex,
                acr: Constants::Auth::LOGIN_GOV_IAL1,
                operation: Constants::Auth::AUTHORIZE)
  Rails.logger.info('[SignIn][Logingov][Service] Rendering auth, ' \
                    "state: #{state}, acr: #{acr}, operation: #{operation}, " \
                    "optional_scopes: #{optional_scopes}")

  scope = (DEFAULT_SCOPES + optional_scopes).join(' ')
  RedirectUrlGenerator.new(redirect_uri: auth_url, params_hash: auth_params(acr, state, scope)).perform
end

#render_logout(client_logout_redirect_uri) ⇒ Object



43
44
45
46
# File 'lib/sign_in/logingov/service.rb', line 43

def render_logout(client_logout_redirect_uri)
  "#{sign_out_url}?#{sign_out_params(config.logout_redirect_uri,
                                     encode_logout_redirect(client_logout_redirect_uri)).to_query}"
end

#render_logout_redirect(state) ⇒ Object



48
49
50
51
52
# File 'lib/sign_in/logingov/service.rb', line 48

def render_logout_redirect(state)
  state_hash = JSON.parse(Base64.decode64(state))
  logout_redirect_uri = state_hash['logout_redirect']
  RedirectUrlGenerator.new(redirect_uri: URI.parse(logout_redirect_uri).to_s).perform
end

#sign_out_params(redirect_uri, state) ⇒ Object (private)



176
177
178
179
180
181
182
# File 'lib/sign_in/logingov/service.rb', line 176

def sign_out_params(redirect_uri, state)
  {
    client_id: config.client_id,
    post_logout_redirect_uri: redirect_uri,
    state:
  }
end

#sign_out_urlObject (private)



172
173
174
# File 'lib/sign_in/logingov/service.rb', line 172

def sign_out_url
  "#{config.base_path}/#{config.logout_path}"
end

#token(code) ⇒ Object



54
55
56
57
58
59
60
61
62
# File 'lib/sign_in/logingov/service.rb', line 54

def token(code)
  response = perform(
    :post, config.token_path, token_params(code), { 'Content-Type' => 'application/json' }
  )
  Rails.logger.info("[SignIn][Logingov][Service] Token Success, code: #{code}")
  parse_token_response(response.body)
rescue Common::Client::Errors::ClientError => e
  raise_client_error(e, 'Token')
end

#token_params(code) ⇒ Object (private)



184
185
186
187
188
189
190
191
# File 'lib/sign_in/logingov/service.rb', line 184

def token_params(code)
  {
    grant_type: config.grant_type,
    code:,
    client_assertion_type: config.client_assertion_type,
    client_assertion: client_assertion_jwt
  }.to_json
end

#token_urlObject (private)



168
169
170
# File 'lib/sign_in/logingov/service.rb', line 168

def token_url
  "#{config.base_path}/#{config.token_path}"
end

#united_states_country_codeObject (private)



121
122
123
# File 'lib/sign_in/logingov/service.rb', line 121

def united_states_country_code
  'USA'
end

#user_info(token) ⇒ Object



64
65
66
67
68
69
70
71
# File 'lib/sign_in/logingov/service.rb', line 64

def (token)
  response = perform(:get, config.userinfo_path, nil, { 'Authorization' => "Bearer #{token}" })
  log_credential(response.body) if config.log_credential

  OpenStruct.new(response.body)
rescue Common::Client::Errors::ClientError => e
  raise_client_error(e, 'UserInfo')
end

#valid_optional_scopes(optional_scopes) ⇒ Object (private)



220
221
222
# File 'lib/sign_in/logingov/service.rb', line 220

def valid_optional_scopes(optional_scopes)
  optional_scopes.to_a & OPTIONAL_SCOPES
end