Class: Inferno::DSL::AuthInfo

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

Overview

AuthInfo provide 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` - Symmetric confidential (i.e., with a static client id and secret)

  • ‘asymmetric` - Symmetric confidential (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

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 # NOT YET IMPLEMENTED
  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.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/inferno/dsl/auth_info.rb', line 130

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 98


#auth_request_method The http method which will be used(Thehttpmethodwhichwillbeused) ⇒ Object

to perform the request to the authorization endpoint. Either ‘get` (default) or `post`



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


#auth_type The type of authorization to be performed.(Thetypeofauthorizationtobeperformed.) ⇒ Object

One of ‘public`, `symmetric`, `asymmetric`, or `backend_services`



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


#auth_url The url of the authorization endpoint(Theurloftheauthorizationendpoint) ⇒ Object



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


#clientObject

Returns the value of attribute client.



96
97
98
# File 'lib/inferno/dsl/auth_info.rb', line 96

def client
  @client
end

#client_idObject



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


#client_secretObject



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


#encryption_algorithm The encryption algorithm which(Theencryptionalgorithmwhich) ⇒ Object

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



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


#expires_in The lifetime of the access token in seconds(Thelifetimeoftheaccesstoken) ⇒ Object



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


#issue_time An iso8601 formatted string representing the(Aniso8601formattedstringrepresentingthe) ⇒ Object

time the access token was issued



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


#jwks A JWKS (including private keys) which will be used(AJWKS(including private keys)) ⇒ Object

instead of Inferno’s default JWKS if provided



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


#kid The key id for the keys to be used to sign the JWT(Thekeyid) ⇒ Object

client credentials. When blank, the first key for the selected encryption algorithm will be used



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


#nameObject



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


#pkce_code_challenge_method Either `S256` (default) or(Either`S256`(default)) ⇒ Object

‘plain`



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


#pkce_support Whether PKCE will be used during(WhetherPKCEwillbeusedduring) ⇒ Object

authorization. Either ‘enabled` or `disabled`.



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


#redirect_urlObject



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


#refresh_tokenObject



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


#requested_scopes The scopes which will be requested(Thescopeswhichwillberequested) ⇒ Object

during authorization



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


#token_url The url of the auth server's token endpoint(Theurloftheauthserver's token endpoint) ⇒ Object



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


Instance Method Details

#able_to_refresh?Boolean

Returns:

  • (Boolean)


183
184
185
# File 'lib/inferno/dsl/auth_info.rb', line 183

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

#add_to_client(client) ⇒ Object



164
165
166
167
168
169
170
171
# File 'lib/inferno/dsl/auth_info.rb', line 164

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



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

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



279
280
281
282
283
284
285
286
287
# File 'lib/inferno/dsl/auth_info.rb', line 279

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



269
270
271
272
273
274
275
276
# File 'lib/inferno/dsl/auth_info.rb', line 269

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

#backend_services?Boolean

Returns:

  • (Boolean)


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

def backend_services?
  auth_type == 'backend_services'
end

#backend_services_auth_refresh_paramsObject



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

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



290
291
292
# File 'lib/inferno/dsl/auth_info.rb', line 290

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

#need_to_refresh?Boolean

Returns:

  • (Boolean)


174
175
176
177
178
179
180
# File 'lib/inferno/dsl/auth_info.rb', line 174

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



238
239
240
241
242
243
244
245
246
247
248
# File 'lib/inferno/dsl/auth_info.rb', line 238

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



193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/inferno/dsl/auth_info.rb', line 193

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



251
252
253
254
255
256
# File 'lib/inferno/dsl/auth_info.rb', line 251

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



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

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

#signing_keyObject



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

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



207
208
209
210
211
212
# File 'lib/inferno/dsl/auth_info.rb', line 207

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

#to_hashObject



147
148
149
150
151
152
153
154
155
156
# File 'lib/inferno/dsl/auth_info.rb', line 147

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



159
160
161
# File 'lib/inferno/dsl/auth_info.rb', line 159

def to_s
  JSON.generate(to_hash)
end

#update_from_response_body(request) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/inferno/dsl/auth_info.rb', line 295

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