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

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
# File 'lib/inferno/dsl/auth_info.rb', line 135

def initialize(raw_attributes_hash)
  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)
  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

Instance Method Details

#able_to_refresh?Boolean

Returns:

  • (Boolean)


188
189
190
# File 'lib/inferno/dsl/auth_info.rb', line 188

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

#add_to_client(client) ⇒ Object



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

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_refresh_paramsObject



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

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



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

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



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

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

#backend_services?Boolean

Returns:

  • (Boolean)


193
194
195
# File 'lib/inferno/dsl/auth_info.rb', line 193

def backend_services?
  auth_type == 'backend_services'
end

#backend_services_auth_refresh_paramsObject



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

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



295
296
297
# File 'lib/inferno/dsl/auth_info.rb', line 295

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

#need_to_refresh?Boolean

Returns:

  • (Boolean)


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

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



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

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



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

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

#private_keyObject



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

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_refresh_paramsObject



220
221
222
# File 'lib/inferno/dsl/auth_info.rb', line 220

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

#signing_keyObject



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

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_refresh_paramsObject



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

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

#to_hashObject



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

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



164
165
166
# File 'lib/inferno/dsl/auth_info.rb', line 164

def to_s
  JSON.generate(to_hash)
end

#update_from_response_body(request) ⇒ Object



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

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)
  self
end