Class: Inferno::DSL::AuthInfo

Inherits:
Object
  • Object
show all
Includes:
Entities::Attributes
Defined in:
lib/inferno/dsl/auth_info.rb

Overview

AuthInfo provides a user with a single input which contains the information needed for a FHIR client to perform authorization and refresh an access token when necessary.

AuthInfo supports the following ‘auth_type`:

- `public` - Client id only
- `symmetric` - Confidential symmetric (i.e., with a static client id and
  secret)
- `asymmetric` - Confidential asymmetric (i.e., a client id with a signed
  JWT rather than a client secret)
- `backend_services`

When configuring an AuthInfo input, the invdidual fields are exposed as ‘components` in the input’s options, and can be configured there similar to normal inputs.

The AuthInfo input type supports two different modes in the UI. Different fields will be presented to the user depending on which mode is selected:

- `auth` - This presents the inputs needed to perform authorization, and
  is appropriate to use as an input to test groups which perform
  authorization.
- `access` - This presents the inputs needed to access resources assuming
  that authorization has already happened, and is appropriate to use as an
  input to test groups which access resources using previously granted
  authorization.

Examples:

class AuthInfoExampleSuite < Inferno::TestSuite
  input :url,
        title: 'Base FHIR url'

  group do
    title 'Perform public authorization'
    input :fhir_auth,
          type: :auth_info,
          options: {
            mode: 'auth',
            components: [
              {
                name: :auth_type,
                default: 'public',
                locked: true
              }
            ]
          }

    # Some tests here to perform authorization
  end

  group do
    title 'FHIR API Tests'
    input :fhir_auth,
          type: :auth_info,
          options: {
            mode: 'access'
          }

    fhir_client do
      url :url
      auth_info :fhir_auth
    end

    # Some tests here to access FHIR API
  end
end

Constant Summary collapse

ATTRIBUTES =
[
  :auth_type,
  :use_discovery,
  :token_url,
  :auth_url,
  :requested_scopes,
  :client_id,
  :client_secret,
  :redirect_url, # TODO: does this belong here?
  :pkce_support,
  :pkce_code_challenge_method,
  :auth_request_method,
  :encryption_algorithm,
  :kid,
  :jwks,
  :access_token,
  :refresh_token,
  :issue_time,
  :expires_in,
  :name
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Entities::Attributes

included

Constructor Details

#initialize(raw_attributes_hash) ⇒ AuthInfo

Returns a new instance of AuthInfo.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/inferno/dsl/auth_info.rb', line 135

def initialize(raw_attributes_hash) # rubocop:disable Metrics/CyclomaticComplexity
  attributes_hash = raw_attributes_hash.symbolize_keys

  invalid_keys = attributes_hash.keys - ATTRIBUTES

  raise Exceptions::UnknownAttributeException.new(invalid_keys, self.class) if invalid_keys.present?

  attributes_hash.each do |name, value|
    value = DateTime.parse(value) if name == :issue_time && value.is_a?(String)

    instance_variable_set(:"@#{name}", value)
  rescue Date::Error
    Inferno::Application['logger'].error("Received invalid date: #{value.inspect}")
  end

  self.issue_time = DateTime.now if access_token.present? && issue_time.blank?
end

Instance Attribute Details

#access_tokenObject



# File 'lib/inferno/dsl/auth_info.rb', line 100

#auth_request_methodObject

The http method which will be used to perform the request to the authorization endpoint. Either ‘get` (default) or `post`



# File 'lib/inferno/dsl/auth_info.rb', line 100

#auth_typeObject

The type of authorization to be performed. One of ‘public`, `symmetric`, `asymmetric`, or `backend_services`



# File 'lib/inferno/dsl/auth_info.rb', line 100

#auth_urlObject

The url of the authorization endpoint



# File 'lib/inferno/dsl/auth_info.rb', line 100

#clientObject

Returns the value of attribute client.



98
99
100
# File 'lib/inferno/dsl/auth_info.rb', line 98

def client
  @client
end

#client_idObject



# File 'lib/inferno/dsl/auth_info.rb', line 100

#client_secretObject



# File 'lib/inferno/dsl/auth_info.rb', line 100

#encryption_algorithmObject

The encryption algorithm which will be used to sign the JWT client credentials. Either ‘es384` (default) or `rs384`



# File 'lib/inferno/dsl/auth_info.rb', line 100

#expires_inObject

The lifetime of the access token in seconds



# File 'lib/inferno/dsl/auth_info.rb', line 100

#issue_timeObject

An iso8601 formatted string representing the time the access token was issued



# File 'lib/inferno/dsl/auth_info.rb', line 100

#jwksObject

A JWKS (including private keys) which will be used instead of Inferno’s default JWKS if provided



# File 'lib/inferno/dsl/auth_info.rb', line 100

#kidObject

The key id for the keys to be used to sign the JWT client credentials. When blank, the first key for the selected encryption algorithm will be used



# File 'lib/inferno/dsl/auth_info.rb', line 100

#nameObject



# File 'lib/inferno/dsl/auth_info.rb', line 100

#pkce_code_challenge_methodObject

Either ‘S256` (default) or `plain`



# File 'lib/inferno/dsl/auth_info.rb', line 100

#pkce_supportObject

Whether PKCE will be used during authorization. Either ‘enabled` or `disabled`.



# File 'lib/inferno/dsl/auth_info.rb', line 100

#redirect_urlObject



# File 'lib/inferno/dsl/auth_info.rb', line 100

#refresh_tokenObject



# File 'lib/inferno/dsl/auth_info.rb', line 100

#requested_scopesObject

The scopes which will be requested during authorization



# File 'lib/inferno/dsl/auth_info.rb', line 100

#token_urlObject

The url of the auth server’s token endpoint



# File 'lib/inferno/dsl/auth_info.rb', line 100

Class Method Details

.default_auth_type_componentHash

Returns the default configuration for the “auth_type” component

Returns:

  • (Hash)


318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/inferno/dsl/auth_info.rb', line 318

def self.default_auth_type_component
  {
    name: :auth_type,
    options: {
      list_options: [
        { label: 'Public', value: 'public' },
        { label: 'Confidential Symmetric', value: 'symmetric' },
        { label: 'Confidential Asymmetric', value: 'asymmetric' },
        { label: 'Backend Services', value: 'backend_services' }
      ]
    }
  }
end

.default_auth_type_component_without_backend_servicesHash

Returns the default configuration for the “auth_type” component without the option for backend services auth

Returns:

  • (Hash)


335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/inferno/dsl/auth_info.rb', line 335

def self.default_auth_type_component_without_backend_services
  {
    name: :auth_type,
    options: {
      list_options: [
        { label: 'Public', value: 'public' },
        { label: 'Confidential Symmetric', value: 'symmetric' },
        { label: 'Confidential Asymmetric', value: 'asymmetric' }
      ]
    }
  }
end

Instance Method Details

#able_to_refresh?Boolean

Returns:

  • (Boolean)


190
191
192
# File 'lib/inferno/dsl/auth_info.rb', line 190

def able_to_refresh?
  token_url.present? && (backend_services? || refresh_token.present?)
end

#add_to_client(client) ⇒ Object



171
172
173
174
175
176
177
178
# File 'lib/inferno/dsl/auth_info.rb', line 171

def add_to_client(client)
  client.auth_info = self
  self.client = client
  # TODO: do we want to perform authorization if no access_token or rely on SMART/ other auth tests?
  return unless access_token.present?

  client.set_bearer_token(access_token)
end

#asymmetric_auth?Boolean

Returns true when using confidential asymmetric auth

Returns:

  • (Boolean)


362
363
364
# File 'lib/inferno/dsl/auth_info.rb', line 362

def asymmetric_auth?
  auth_type&.casecmp? 'asymmetric'
end

#asymmetric_auth_refresh_paramsObject



227
228
229
230
231
232
# File 'lib/inferno/dsl/auth_info.rb', line 227

def asymmetric_auth_refresh_params
  symmetric_auth_refresh_params.merge(
    'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    'client_assertion' => client_assertion
  )
end

#auth_jwt_claimsObject



286
287
288
289
290
291
292
293
294
# File 'lib/inferno/dsl/auth_info.rb', line 286

def auth_jwt_claims
  {
    'iss' => client_id,
    'sub' => client_id,
    'aud' => token_url,
    'exp' => 5.minutes.from_now.to_i,
    'jti' => SecureRandom.hex(32)
  }
end

#auth_jwt_headerObject



276
277
278
279
280
281
282
283
# File 'lib/inferno/dsl/auth_info.rb', line 276

def auth_jwt_header
  {
    'alg' => encryption_algorithm,
    'kid' => private_key['kid'],
    'typ' => 'JWT',
    'jku' => Inferno::Application['jwks_url']
  }
end

#backend_services?Boolean

Returns:

  • (Boolean)


195
196
197
# File 'lib/inferno/dsl/auth_info.rb', line 195

def backend_services?
  auth_type == 'backend_services'
end

#backend_services_auth?Boolean

Returns true when using backend services auth

Returns:

  • (Boolean)


368
369
370
# File 'lib/inferno/dsl/auth_info.rb', line 368

def backend_services_auth?
  auth_type&.casecmp? 'backend_services'
end

#backend_services_auth_refresh_paramsObject



235
236
237
238
239
240
241
242
# File 'lib/inferno/dsl/auth_info.rb', line 235

def backend_services_auth_refresh_params
  {
    'grant_type' => 'client_credentials',
    'scope' => requested_scopes,
    'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    'client_assertion' => client_assertion
  }
end

#client_assertionObject



297
298
299
# File 'lib/inferno/dsl/auth_info.rb', line 297

def client_assertion
  JWT.encode auth_jwt_claims, signing_key, encryption_algorithm, auth_jwt_header
end

#get_auth_request?Boolean

Returns true when using GET as the authorization request method

Returns:

  • (Boolean)


374
375
376
# File 'lib/inferno/dsl/auth_info.rb', line 374

def get_auth_request?
  auth_request_method&.casecmp? 'get'
end

#need_to_refresh?Boolean

Returns:

  • (Boolean)


181
182
183
184
185
186
187
# File 'lib/inferno/dsl/auth_info.rb', line 181

def need_to_refresh?
  return false if access_token.blank? || (!backend_services? && refresh_token.blank?)

  return true if expires_in.blank?

  issue_time.to_i + expires_in.to_i - DateTime.now.to_i < 60
end

#oauth2_refresh_headersObject



245
246
247
248
249
250
251
252
253
254
255
# File 'lib/inferno/dsl/auth_info.rb', line 245

def oauth2_refresh_headers
  base_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }

  return base_headers unless auth_type == 'symmetric'

  credentials = "#{client_id}:#{client_secret}"

  base_headers.merge(
    'Authorization' => "Basic #{Base64.strict_encode64(credentials)}"
  )
end

#oauth2_refresh_paramsObject



200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/inferno/dsl/auth_info.rb', line 200

def oauth2_refresh_params
  case auth_type
  when 'public'
    public_auth_refresh_params
  when 'symmetric'
    symmetric_auth_refresh_params
  when 'asymmetric'
    asymmetric_auth_refresh_params
  when 'backend_services'
    backend_services_auth_refresh_params
  end
end

#pkce_enabled?Boolean

Returns true when pkce is enabled

Returns:

  • (Boolean)


386
387
388
# File 'lib/inferno/dsl/auth_info.rb', line 386

def pkce_enabled?
  pkce_support&.casecmp? 'enabled'
end

#plain_code_challenge_method?Boolean

Returns true when using the palin pkce code challenge method

Returns:

  • (Boolean)


398
399
400
# File 'lib/inferno/dsl/auth_info.rb', line 398

def plain_code_challenge_method?
  pkce_code_challenge_method&.casecmp? 'plain'
end

#post_auth_request?Boolean

Returns true when using POST as the authorization request method

Returns:

  • (Boolean)


380
381
382
# File 'lib/inferno/dsl/auth_info.rb', line 380

def post_auth_request?
  auth_request_method&.casecmp? 'post'
end

#private_keyObject



258
259
260
261
262
263
# File 'lib/inferno/dsl/auth_info.rb', line 258

def private_key
  @private_key ||= JWKS.jwks(user_jwks: jwks)
    .select { |key| key[:key_ops]&.include?('sign') }
    .select { |key| key[:alg] == encryption_algorithm }
    .find { |key| !kid || key[:kid] == kid }
end

#public_auth?Boolean

Returns true when using public auth

Returns:

  • (Boolean)


350
351
352
# File 'lib/inferno/dsl/auth_info.rb', line 350

def public_auth?
  auth_type&.casecmp? 'public'
end

#public_auth_refresh_paramsObject



222
223
224
# File 'lib/inferno/dsl/auth_info.rb', line 222

def public_auth_refresh_params
  symmetric_auth_refresh_params.merge('client_id' => client_id)
end

#s256_code_challenge_method?Boolean

Returns true when using the S256 pkce code challenge method

Returns:

  • (Boolean)


392
393
394
# File 'lib/inferno/dsl/auth_info.rb', line 392

def s256_code_challenge_method?
  pkce_code_challenge_method&.casecmp? 'S256'
end

#signing_keyObject



266
267
268
269
270
271
272
273
# File 'lib/inferno/dsl/auth_info.rb', line 266

def signing_key
  if private_key.nil?
    raise Inferno::Exceptions::AssertionException,
          "No signing key found for inputs: encryption method = '#{encryption_algorithm}' and kid = '#{kid}'"
  end

  @private_key.signing_key
end

#symmetric_auth?Boolean

Returns true when using confidential symmetric auth

Returns:

  • (Boolean)


356
357
358
# File 'lib/inferno/dsl/auth_info.rb', line 356

def symmetric_auth?
  auth_type&.casecmp? 'symmetric'
end

#symmetric_auth_refresh_paramsObject



214
215
216
217
218
219
# File 'lib/inferno/dsl/auth_info.rb', line 214

def symmetric_auth_refresh_params
  {
    'grant_type' => 'refresh_token',
    'refresh_token' => refresh_token
  }
end

#to_hashObject



154
155
156
157
158
159
160
161
162
163
# File 'lib/inferno/dsl/auth_info.rb', line 154

def to_hash
  self.class::ATTRIBUTES.each_with_object({}) do |attribute, hash|
    value = send(attribute)
    next if value.nil?

    value = issue_time.iso8601 if attribute == :issue_time

    hash[attribute] = value
  end
end

#to_sObject



166
167
168
# File 'lib/inferno/dsl/auth_info.rb', line 166

def to_s
  JSON.generate(to_hash)
end

#update_from_response_body(request) ⇒ Object



302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/inferno/dsl/auth_info.rb', line 302

def update_from_response_body(request)
  token_response_body = JSON.parse(request.response_body)

  expires_in = token_response_body['expires_in'].is_a?(Numeric) ? token_response_body['expires_in'] : nil

  self.access_token = token_response_body['access_token']
  self.refresh_token = token_response_body['refresh_token'] if token_response_body['refresh_token'].present?
  self.expires_in = expires_in
  self.issue_time = DateTime.now

  add_to_client(client) if client
  self
end