smart_app_launch_test_kit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module SMARTAppLaunch
2
+ class LaunchReceivedTest < Inferno::Test
3
+ title 'EHR server sends launch parameter'
4
+ description %(
5
+ Code is a required querystring parameter on the redirect.
6
+ )
7
+ id :smart_launch_received
8
+
9
+ output :launch
10
+ uses_request :launch
11
+
12
+ run do
13
+ launch = request.query_parameters['launch']
14
+ output launch: launch
15
+
16
+ assert launch.present?, 'No `launch` paramater received'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ require 'jwt'
2
+ require_relative 'openid_decode_id_token_test'
3
+ require_relative 'openid_retrieve_configuration_test'
4
+ require_relative 'openid_required_configuration_fields_test'
5
+ require_relative 'openid_retrieve_jwks_test'
6
+ require_relative 'openid_token_header_test'
7
+ require_relative 'openid_token_payload_test'
8
+ require_relative 'openid_fhir_user_claim_test'
9
+
10
+ module SMARTAppLaunch
11
+ class OpenIDConnectGroup < Inferno::TestGroup
12
+ id :smart_openid_connect
13
+ title 'OpenID Connect'
14
+
15
+ description %(
16
+ # Background
17
+
18
+ OpenID Connect (OIDC) provides the ability to verify the identity of the
19
+ authorizing user. Within the [SMART App Launch
20
+ Framework](http://hl7.org/fhir/smart-app-launch/), Applications can
21
+ request an `id_token` be provided with by including the `openid fhirUser`
22
+ scopes when requesting authorization.
23
+
24
+ # Test Methodology
25
+
26
+ This sequence validates the id token returned as part of the OAuth 2.0
27
+ token response. Once the token is decoded, the server's OIDC configuration
28
+ is retrieved from its well-known configuration endpoint. This
29
+ configuration is checked to ensure that all required fields are present.
30
+ Next the keys used to cryptographically sign the id token are retrieved
31
+ from the url contained in the OIDC configuration. Then the header,
32
+ payload, and signature of the id token are validated. Finally, the FHIR
33
+ resource from the `fhirUser` claim in the id token is fetched from the
34
+ FHIR server.
35
+
36
+ For more information see:
37
+
38
+ * [SMART App Launch Framework](http://hl7.org/fhir/smart-app-launch/)
39
+ * [Scopes for requesting identity data](http://hl7.org/fhir/smart-app-launch/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
40
+ * [Apps Requesting Authorization](http://hl7.org/fhir/smart-app-launch/#step-1-app-asks-for-authorization)
41
+ * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
42
+ )
43
+
44
+ test from: :smart_openid_decode_id_token
45
+
46
+ test from: :smart_openid_retrieve_configuration
47
+
48
+ test from: :smart_openid_required_configuration_fields
49
+
50
+ test from: :smart_openid_retrieve_jwks
51
+
52
+ test from: :smart_openid_token_header
53
+
54
+ test from: :smart_openid_token_payload
55
+
56
+ test from: :smart_openid_fhir_user_claim
57
+
58
+ # test do
59
+ # id :smart_openid_fhir_user_retrieval
60
+ # title 'fhirUser can be retrieved'
61
+ # description %(
62
+ # Verify that the FHIR resource referred to in the `fhirUser` claim can be
63
+ # retrieved.
64
+ # )
65
+
66
+ # input :id_token_fhir_user, :openid_issuer, :standalone_access_token
67
+ # makes_request :id_token_fhir_user
68
+
69
+ # run do
70
+ # skip_if id_token_fhir_user.blank?
71
+
72
+ # split_fhir_user = id_token_fhir_user.split('/')
73
+ # resource_type = split_fhir_user[-2]
74
+ # resource_id = split_fhir_user[-1]
75
+ # fhir_read(resource_type, resource_id)
76
+
77
+ # assert_response_status(200)
78
+ # assert_valid_json(response[:body])
79
+ # assert_resource_type(resource_type)
80
+ # end
81
+ # end
82
+ end
83
+ end
@@ -0,0 +1,30 @@
1
+ module SMARTAppLaunch
2
+ class OpenIDDecodeIDTokenTest < Inferno::Test
3
+ id :smart_openid_decode_id_token
4
+ title 'ID token can be decoded'
5
+ description %(
6
+ Verify that the ID token is a properly constructed JWT.
7
+ )
8
+
9
+ input :id_token
10
+ output :id_token_payload_json, :id_token_header_json
11
+
12
+ run do
13
+ skip_if id_token.blank?
14
+
15
+ begin
16
+ payload, header =
17
+ JWT.decode(
18
+ id_token,
19
+ nil,
20
+ false
21
+ )
22
+
23
+ output id_token_payload_json: payload.to_json,
24
+ id_token_header_json: header.to_json
25
+ rescue StandardError => e
26
+ assert false, "ID token is not a properly constructed JWT: #{e.message}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ module SMARTAppLaunch
2
+ class OpenIDFHIRUserClaimTest < Inferno::Test
3
+ id :smart_openid_fhir_user_claim
4
+ title 'ID token contains a valid fhirUser claim'
5
+ description %(
6
+ Verify that the `fhirUser` claim is present in the ID token. The
7
+ `fhirUser` claim must be the url for a Patient, Practitioner,
8
+ RelatedPerson, or Person resource.
9
+ )
10
+
11
+ input :id_token_payload_json, :requested_scopes
12
+ output :id_token_fhir_user
13
+
14
+ run do
15
+ skip_if id_token_payload_json.blank?
16
+ skip_if !requested_scopes&.include?('fhirUser'), '`fhirUser` scope not requested'
17
+
18
+ payload = JSON.parse(id_token_payload_json)
19
+ fhir_user = payload['fhirUser']
20
+
21
+ valid_fhir_user_resource_types = ['Patient', 'Practitioner', 'RelatedPerson', 'Person']
22
+
23
+ assert fhir_user.present?, 'ID token does not contain `fhirUser` claim'
24
+ assert valid_fhir_user_resource_types.any? { |type| fhir_user.include? type },
25
+ "ID token `fhirUser` claim does not refer to a valid resource type: #{fhir_user}"
26
+
27
+ output id_token_fhir_user: fhir_user
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,50 @@
1
+ module SMARTAppLaunch
2
+ class OpenIDRequiredConfigurationFieldsTest < Inferno::Test
3
+ id :smart_openid_required_configuration_fields
4
+ title 'OpenID Connect well-known configuration contains all required fields'
5
+ description %(
6
+ Verify that the OpenId Connect configuration contains the following
7
+ required fields: `issuer`, `authorization_endpoint`, `token_endpoint`,
8
+ `jwks_uri`, `response_types_supported`, `subject_types_supported`, and
9
+ `id_token_signing_alg_values_supported`.
10
+
11
+ Additionally, the [SMART App Launch
12
+ Framework](http://www.hl7.org/fhir/smart-app-launch/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
13
+ requires that the RSA SHA-256 signing algorithm be supported.
14
+ )
15
+
16
+ input :openid_configuration_json
17
+ output :openid_jwks_uri
18
+
19
+ REQUIRED_FIELDS =
20
+ [
21
+ 'issuer',
22
+ 'authorization_endpoint',
23
+ 'token_endpoint',
24
+ 'jwks_uri',
25
+ 'response_types_supported',
26
+ 'subject_types_supported',
27
+ 'id_token_signing_alg_values_supported'
28
+ ].freeze
29
+
30
+ def required_fields
31
+ REQUIRED_FIELDS.dup
32
+ end
33
+
34
+ run do
35
+ skip_if openid_configuration_json.blank?
36
+
37
+ configuration = JSON.parse(openid_configuration_json)
38
+ output openid_jwks_uri: configuration['jwks_uri']
39
+
40
+ missing_fields = required_fields - configuration.keys
41
+ missing_fields_string = missing_fields.map { |field| "`#{field}`" }.join(', ')
42
+
43
+ assert missing_fields.empty?,
44
+ "OpenID Connect well-known configuration missing required fields: #{missing_fields_string}"
45
+
46
+ assert configuration['id_token_signing_alg_values_supported'].include?('RS256'),
47
+ 'Signing tokens with RSA SHA-256 not supported'
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ module SMARTAppLaunch
2
+ class OpenIDRetrieveConfigurationTest < Inferno::Test
3
+ id :smart_openid_retrieve_configuration
4
+ title 'OpenID Connect well-known configuration can be retrieved'
5
+ description %(
6
+ Verify that the OpenId Connect configuration can be retrieved as
7
+ described in the OpenID Connect Discovery 1.0 documentation.
8
+ )
9
+
10
+ input :id_token_payload_json
11
+ output :openid_configuration_json, :openid_issuer
12
+ makes_request :openid_configuration
13
+
14
+ run do
15
+ skip_if id_token_payload_json.blank?
16
+
17
+ payload = JSON.parse(id_token_payload_json)
18
+ issuer = payload['iss']
19
+
20
+ configuration_url = "#{issuer.chomp('/')}/.well-known/openid-configuration"
21
+ get(configuration_url, name: :openid_configuration)
22
+
23
+ assert_response_status(200)
24
+ assert_response_content_type('application/json')
25
+ assert_valid_json(response[:body])
26
+
27
+ output openid_configuration_json: response[:body],
28
+ openid_issuer: issuer
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ module SMARTAppLaunch
2
+ class OpenIDRetrieveJWKSTest < Inferno::Test
3
+ id :smart_openid_retrieve_jwks
4
+ title 'JWKS can be retrieved'
5
+ description %(
6
+ Verify that the JWKS can be retrieved from the `jwks_uri` from the
7
+ OpenID Connect well-known configuration.
8
+ )
9
+
10
+ input :openid_jwks_uri
11
+ output :openid_jwks_json, :openid_rsa_keys_json
12
+ makes_request :openid_jwks
13
+
14
+ run do
15
+ skip_if openid_jwks_uri.blank?
16
+
17
+ get(openid_jwks_uri, name: :openid_jwks)
18
+
19
+ assert_response_status(200)
20
+ assert_valid_json(response[:body])
21
+ output openid_jwks_json: response[:body]
22
+
23
+ raw_jwks = JSON.parse(response[:body])
24
+ assert raw_jwks['keys'].is_a?(Array), 'JWKS `keys` field must be an array'
25
+
26
+ # https://tools.ietf.org/html/rfc7517#section-5
27
+ # Implementations SHOULD ignore JWKs within a JWK Set that use "kty"
28
+ # (key type) values that are not understood by them.
29
+ # SMART only requires support of RSA SHA-256 keys
30
+ rsa_keys = raw_jwks['keys'].select { |jwk| jwk['kty'] == 'RSA' }
31
+
32
+ assert rsa_keys.present?, 'JWKS contains no RSA keys'
33
+
34
+ rsa_keys.each do |jwk|
35
+ JWT::JWK.import(jwk.deep_symbolize_keys)
36
+ rescue StandardError
37
+ assert false, "Invalid JWK: #{jwk.to_json}"
38
+ end
39
+
40
+ output openid_rsa_keys_json: rsa_keys.to_json
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module SMARTAppLaunch
2
+ class OpenIDTokenHeaderTest < Inferno::Test
3
+ id :smart_openid_token_header
4
+ title 'ID token header contains required information'
5
+ description %(
6
+ Verify that the id token header indicates that the tokenis signed using
7
+ RSA SHA-256 [as required by the SMART app launch
8
+ framework](http://www.hl7.org/fhir/smart-app-launch/scopes-and-launch-context/index.html#scopes-for-requesting-identity-data)
9
+ and that the key used to sign the token can be identified in the JWKS.
10
+ )
11
+
12
+ input :id_token_header_json, :openid_rsa_keys_json
13
+ output :id_token_jwk_json
14
+
15
+ run do
16
+ skip_if id_token_header_json.blank?
17
+ skip_if openid_rsa_keys_json.blank?
18
+
19
+ header = JSON.parse(id_token_header_json)
20
+ algorithm = header['alg']
21
+
22
+ assert algorithm == 'RS256', "ID Token signed with `#{algorithm}` rather than RS256"
23
+
24
+ kid = header['kid']
25
+ rsa_keys = JSON.parse(openid_rsa_keys_json)
26
+
27
+ if rsa_keys.length > 1
28
+ assert kid.present?, '`kid` field must be present if JWKS contains multiple keys'
29
+ jwk = rsa_keys.find { |key| key['kid'] == kid }
30
+ assert jwk.present?, "JWKS did not contain an RS256 key with an id of `#{kid}`"
31
+ else
32
+ jwk = rsa_keys.first
33
+ assert kid.blank? || jwk['kid'] == kid, "JWKS did not contain an RS256 key with an id of `#{kid}`"
34
+ end
35
+
36
+ output id_token_jwk_json: jwk.to_json
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'token_payload_validation'
2
+
3
+ module SMARTAppLaunch
4
+ class OpenIDTokenPayloadTest < Inferno::Test
5
+ include TokenPayloadValidation
6
+ id :smart_openid_token_payload
7
+ title 'ID token payload has required claims and a valid signature'
8
+ description %(
9
+ The `iss`, `sub`, `aud`, `exp`, and `iat` claims are required.
10
+ Additionally:
11
+
12
+ - `iss` must match the `issuer` from the OpenID Connect well-known
13
+ configuration
14
+ - `aud` must match the client ID
15
+ - `exp` must represent a time in the future
16
+ )
17
+
18
+ REQUIRED_CLAIMS = ['iss', 'sub', 'aud', 'exp', 'iat'].freeze
19
+
20
+ def required_claims
21
+ REQUIRED_CLAIMS.dup
22
+ end
23
+
24
+ input :id_token,
25
+ :openid_configuration_json,
26
+ :id_token_jwk_json,
27
+ :client_id
28
+
29
+ run do
30
+ skip_if id_token.blank?, 'No ID Token'
31
+ skip_if openid_configuration_json.blank?, 'No OpenID Configuration found'
32
+ skip_if id_token_jwk_json.blank?, 'No ID Token jwk found'
33
+ skip_if client_id.blank?, 'No Client ID'
34
+
35
+ begin
36
+ configuration = JSON.parse(openid_configuration_json)
37
+ jwk = JSON.parse(id_token_jwk_json).deep_symbolize_keys
38
+ payload, =
39
+ JWT.decode(
40
+ id_token,
41
+ JWT::JWK.import(jwk).public_key,
42
+ true,
43
+ algorithms: ['RS256'],
44
+ exp_leeway: 60,
45
+ iss: configuration['issuer'],
46
+ aud: client_id,
47
+ verify_not_before: false,
48
+ verify_iat: false,
49
+ verify_jti: false,
50
+ verify_sub: false,
51
+ verify_iss: true,
52
+ verify_aud: true
53
+ )
54
+ rescue StandardError => e
55
+ assert false, "Token validation error: #{e.message}"
56
+ end
57
+
58
+ missing_claims = required_claims - payload.keys
59
+ missing_claims_string = missing_claims.map { |claim| "`#{claim}`" }.join(', ')
60
+
61
+ assert missing_claims.empty?, "ID token missing required claims: #{missing_claims_string}"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,104 @@
1
+ require_relative 'app_redirect_test'
2
+ require_relative 'code_received_test'
3
+ require_relative 'token_exchange_test'
4
+ require_relative 'token_response_body_test'
5
+ require_relative 'token_response_headers_test'
6
+
7
+ module SMARTAppLaunch
8
+ class StandaloneLaunchGroup < Inferno::TestGroup
9
+ id :smart_standalone_launch
10
+ title 'SMART Standalone Launch'
11
+
12
+ description %(
13
+ # Background
14
+
15
+ The [Standalone
16
+ Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
17
+ Sequence allows an app, like Inferno, to be launched independent of an
18
+ existing EHR session. It is one of the two launch methods described in
19
+ the SMART App Launch Framework alongside EHR Launch. The app will
20
+ request authorization for the provided scope from the authorization
21
+ endpoint, ultimately receiving an authorization token which can be used
22
+ to gain access to resources on the FHIR server.
23
+
24
+ # Test Methodology
25
+
26
+ Inferno will redirect the user to the the authorization endpoint so that
27
+ they may provide any required credentials and authorize the application.
28
+ Upon successful authorization, Inferno will exchange the authorization
29
+ code provided for an access token.
30
+
31
+ For more information on the #{title}:
32
+
33
+ * [Standalone Launch Sequence](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
34
+ )
35
+
36
+ config(
37
+ inputs: {
38
+ client_id: {
39
+ name: :standalone_client_id,
40
+ title: 'Standalone Client ID',
41
+ description: 'Client ID provided during registration of Inferno as a standalone application',
42
+ default: 'SAMPLE_PUBLIC_CLIENT_ID'
43
+ },
44
+ client_secret: {
45
+ name: :standalone_client_secret,
46
+ title: 'Standalone Client Secret',
47
+ description: 'Client Secret provided during registration of Inferno as a standalone application'
48
+ },
49
+ requested_scopes: {
50
+ name: :standalone_requested_scopes,
51
+ title: 'Standalone Scope',
52
+ description: 'OAuth 2.0 scope provided by system to enable all required functionality',
53
+ type: 'textarea',
54
+ default: %(
55
+ launch/patient openid fhirUser offline_access
56
+ patient/Medication.read patient/AllergyIntolerance.read
57
+ patient/CarePlan.read patient/CareTeam.read patient/Condition.read
58
+ patient/Device.read patient/DiagnosticReport.read
59
+ patient/DocumentReference.read patient/Encounter.read
60
+ patient/Goal.read patient/Immunization.read patient/Location.read
61
+ patient/MedicationRequest.read patient/Observation.read
62
+ patient/Organization.read patient/Patient.read
63
+ patient/Practitioner.read patient/Procedure.read
64
+ patient/Provenance.read patient/PractitionerRole.read
65
+ ).gsub(/\s{2,}/, ' ').strip
66
+ },
67
+ url: {
68
+ title: 'Standalone FHIR Endpoint',
69
+ description: 'URL of the FHIR endpoint used by standalone applications',
70
+ default: 'https://inferno.healthit.gov/reference-server/r4'
71
+ },
72
+ code: {
73
+ name: :standalone_code
74
+ },
75
+ state: {
76
+ name: :standalone_state
77
+ }
78
+ },
79
+ outputs: {
80
+ code: { name: :standalone_code },
81
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
82
+ state: { name: :standalone_state },
83
+ id_token: { name: :standalone_id_token },
84
+ refresh_token: { name: :standalone_refresh_token },
85
+ access_token: { name: :standalone_access_token },
86
+ expires_in: { name: :standalone_expires_in },
87
+ patient_id: { name: :standalone_patient_id },
88
+ encounter_id: { name: :standalone_encounter_id },
89
+ received_scopes: { name: :standalone_received_scopes },
90
+ intent: { name: :standalone_intent }
91
+ },
92
+ requests: {
93
+ redirect: { name: :standalone_redirect },
94
+ token: { name: :standalone_token }
95
+ }
96
+ )
97
+
98
+ test from: :smart_app_redirect
99
+ test from: :smart_code_received
100
+ test from: :smart_token_exchange
101
+ test from: :smart_token_response_body
102
+ test from: :smart_token_response_headers
103
+ end
104
+ end
@@ -0,0 +1,47 @@
1
+ module SMARTAppLaunch
2
+ class TokenExchangeTest < Inferno::Test
3
+ title 'OAuth token exchange request succeeds when supplied correct information'
4
+ description %(
5
+ After obtaining an authorization code, the app trades the code for an
6
+ access token via HTTP POST to the EHR authorization server's token
7
+ endpoint URL, using content-type application/x-www-form-urlencoded, as
8
+ described in section [4.1.3 of
9
+ RFC6749](https://tools.ietf.org/html/rfc6749#section-4.1.3).
10
+ )
11
+ id :smart_token_exchange
12
+
13
+ input :code,
14
+ :smart_token_url,
15
+ :client_id
16
+ input :client_secret, optional: true
17
+ output :token_retrieval_time
18
+ uses_request :redirect
19
+ makes_request :token
20
+
21
+ config options: { redirect_uri: "#{Inferno::Application['inferno_host']}/custom/smart/redirect" }
22
+
23
+ run do
24
+ skip_if request.query_parameters['error'].present?, 'Error during authorization request'
25
+
26
+ oauth2_params = {
27
+ grant_type: 'authorization_code',
28
+ code: code,
29
+ redirect_uri: config.options[:redirect_uri]
30
+ }
31
+ oauth2_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
32
+
33
+ if client_secret.present?
34
+ client_credentials = "#{client_id}:#{client_secret}"
35
+ oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
36
+ else
37
+ oauth2_params[:client_id] = client_id
38
+ end
39
+
40
+ post(smart_token_url, body: oauth2_params, name: :token, headers: oauth2_headers)
41
+
42
+ output token_retrieval_time: Time.now.iso8601
43
+
44
+ assert_response_status(200)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ module SMARTAppLaunch
2
+ module TokenPayloadValidation
3
+ STRING_FIELDS = ['access_token', 'token_type', 'scope', 'refresh_token'].freeze
4
+ NUMERIC_FIELDS = ['expires_in'].freeze
5
+
6
+ def validate_required_fields_present(body, required_fields)
7
+ missing_fields = required_fields.select { |field| body[field].blank? }
8
+ missing_fields_string = missing_fields.map { |field| "`#{field}`" }.join(', ')
9
+ assert missing_fields.empty?,
10
+ "Token exchange response did not include all required fields: #{missing_fields_string}."
11
+ end
12
+
13
+ def validate_token_type(body)
14
+ assert body['token_type'].casecmp('bearer').zero?, '`token_type` must be `bearer`'
15
+ end
16
+
17
+ def check_for_missing_scopes(requested_scopes, body)
18
+ expected_scopes = requested_scopes.split
19
+ new_scopes = body['scope'].split
20
+ missing_scopes = expected_scopes - new_scopes
21
+
22
+ warning do
23
+ missing_scopes_string = missing_scopes.map { |scope| "`#{scope}`" }.join(', ')
24
+ assert missing_scopes.empty?, %(
25
+ Token exchange response did not include all requested scopes.
26
+ These may have been denied by user: #{missing_scopes_string}.
27
+ )
28
+ end
29
+ end
30
+
31
+ def validate_token_field_types(body)
32
+ STRING_FIELDS
33
+ .select { |field| body[field].present? }
34
+ .each do |field|
35
+ assert body[field].is_a?(String),
36
+ "Expected `#{field}` to be a String, but found #{body[field].class.name}"
37
+ end
38
+
39
+ NUMERIC_FIELDS
40
+ .select { |field| body[field].present? }
41
+ .each do |field|
42
+ assert body[field].is_a?(Numeric),
43
+ "Expected `#{field}` to be a Numeric, but found #{body[field].class.name}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,52 @@
1
+ require_relative 'token_payload_validation'
2
+
3
+ module SMARTAppLaunch
4
+ class TokenRefreshBodyTest < Inferno::Test
5
+ include TokenPayloadValidation
6
+
7
+ id :smart_token_refresh_body
8
+ title 'Server successfully refreshes the access token when optional scope parameter omitted'
9
+ description %(
10
+ Server successfully exchanges refresh token at OAuth token endpoint
11
+ without providing scope in the body of the request.
12
+
13
+ The EHR authorization server SHALL return a JSON structure that includes
14
+ an access token or a message indicating that the authorization request
15
+ has been denied. `access_token`, `expires_in`, `token_type`, and `scope` are
16
+ required. `access_token` must be `Bearer`.
17
+
18
+ Although not required in the token refresh portion of the SMART App
19
+ Launch Guide, the token refresh response should include the HTTP
20
+ Cache-Control response header field with a value of no-store, as well as
21
+ the Pragma response header field with a value of no-cache to be
22
+ consistent with the requirements of the inital access token exchange.
23
+ )
24
+ input :received_scopes
25
+ output :refresh_token, :access_token, :token_retrieval_time, :expires_in, :received_scopes
26
+ uses_request :token_refresh
27
+
28
+ run do
29
+ skip_if request.status != 200, 'Token exchange was unsuccessful'
30
+
31
+ assert_valid_json(response[:body])
32
+
33
+ body = JSON.parse(response[:body])
34
+ output refresh_token: body['refresh_token'] if body.key? 'refresh_token'
35
+
36
+ required_fields = ['access_token', 'token_type', 'expires_in', 'scope']
37
+ validate_required_fields_present(body, required_fields)
38
+
39
+ old_received_scopes = received_scopes
40
+ output access_token: body['access_token'],
41
+ token_retrieval_time: Time.now.iso8601,
42
+ expires_in: body['expires_in'],
43
+ received_scopes: body['scope']
44
+
45
+ validate_token_field_types(body)
46
+ validate_token_type(body)
47
+
48
+ assert received_scopes.split.sort == old_received_scopes.split.sort,
49
+ 'Received scopes not equal to originally granted scopes'
50
+ end
51
+ end
52
+ end