smart_app_launch_test_kit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/smart_app_launch/app_redirect_test.rb +2 -0
- data/lib/smart_app_launch/client_assertion_builder.rb +63 -0
- data/lib/smart_app_launch/code_received_test.rb +1 -1
- data/lib/smart_app_launch/ehr_launch_group.rb +2 -1
- data/lib/smart_app_launch/ehr_launch_group_stu2.rb +8 -0
- data/lib/smart_app_launch/jwks.rb +27 -0
- data/lib/smart_app_launch/launch_received_test.rb +3 -2
- data/lib/smart_app_launch/smart_jwks.json +58 -0
- data/lib/smart_app_launch/smart_stu1_suite.rb +33 -0
- data/lib/smart_app_launch/smart_stu2_suite.rb +48 -0
- data/lib/smart_app_launch/standalone_launch_group.rb +2 -1
- data/lib/smart_app_launch/standalone_launch_group_stu2.rb +8 -0
- data/lib/smart_app_launch/token_exchange_stu2_test.rb +91 -0
- data/lib/smart_app_launch/token_exchange_test.rb +12 -8
- data/lib/smart_app_launch/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97f3e4dca10ecb9dfe4aa30802d90f4272dd32ffd10715fa7b3a0855a04b5c8d
|
4
|
+
data.tar.gz: 016a700551ef524e7bdc7f8871beb03f7297dc3cba39e5b4b53349b22f22ed53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 519d2bb8c5bde6a7c5c28f04ec17bfdcff37c7270b6216707a05db4b04c94269d244c6a1888dd97a0c25c5a50764d4969a08c2774dcb636db7fd274301184427
|
7
|
+
data.tar.gz: eda1f5484f72de3a82c34eb1d0424a2b10a327baeb93d6e6df8f94be7a7e3ef378e5e6510378f62e7d9387e56424b42b52f4524fc557302d3a5ddab0654c782e
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
require_relative 'jwks'
|
4
|
+
|
5
|
+
module SMARTAppLaunch
|
6
|
+
class ClientAssertionBuilder
|
7
|
+
def self.build(...)
|
8
|
+
new(...).client_assertion
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :aud,
|
12
|
+
:client_assertion_type,
|
13
|
+
:content_type,
|
14
|
+
:client_auth_encryption_method,
|
15
|
+
:exp,
|
16
|
+
:grant_type,
|
17
|
+
:iss,
|
18
|
+
:jti,
|
19
|
+
:sub
|
20
|
+
|
21
|
+
def initialize(
|
22
|
+
client_auth_encryption_method:,
|
23
|
+
iss:,
|
24
|
+
sub:,
|
25
|
+
aud:,
|
26
|
+
exp: 5.minutes.from_now.to_i,
|
27
|
+
jti: SecureRandom.hex(32)
|
28
|
+
)
|
29
|
+
@client_auth_encryption_method = client_auth_encryption_method
|
30
|
+
@iss = iss
|
31
|
+
@sub = sub
|
32
|
+
@aud = aud
|
33
|
+
@content_type = content_type
|
34
|
+
@grant_type = grant_type
|
35
|
+
@client_assertion_type = client_assertion_type
|
36
|
+
@exp = exp
|
37
|
+
@jti = jti
|
38
|
+
end
|
39
|
+
|
40
|
+
def private_key
|
41
|
+
@private_key ||=
|
42
|
+
JWKS.jwks
|
43
|
+
.find { |key| key[:key_ops]&.include?('sign') && key[:alg] == client_auth_encryption_method }
|
44
|
+
end
|
45
|
+
|
46
|
+
def jwt_payload
|
47
|
+
{ iss:, sub:, aud:, exp:, jti: }.compact
|
48
|
+
end
|
49
|
+
|
50
|
+
def kid
|
51
|
+
private_key.kid
|
52
|
+
end
|
53
|
+
|
54
|
+
def signing_key
|
55
|
+
private_key.signing_key
|
56
|
+
end
|
57
|
+
|
58
|
+
def client_assertion
|
59
|
+
@client_assertion ||=
|
60
|
+
JWT.encode jwt_payload, signing_key, client_auth_encryption_method, { alg: client_auth_encryption_method, kid:, typ: 'JWT' }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -48,7 +48,8 @@ module SMARTAppLaunch
|
|
48
48
|
client_secret: {
|
49
49
|
name: :ehr_client_secret,
|
50
50
|
title: 'EHR Launch Client Secret',
|
51
|
-
description: 'Client Secret provided during registration of Inferno as an EHR launch application'
|
51
|
+
description: 'Client Secret provided during registration of Inferno as an EHR launch application. ' \
|
52
|
+
'Only for clients using confidential symmetric authentication.'
|
52
53
|
},
|
53
54
|
requested_scopes: {
|
54
55
|
name: :ehr_requested_scopes,
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require_relative 'app_redirect_test_stu2'
|
2
2
|
require_relative 'ehr_launch_group'
|
3
|
+
require_relative 'token_exchange_stu2_test'
|
3
4
|
|
4
5
|
module SMARTAppLaunch
|
5
6
|
class EHRLaunchGroupSTU2 < EHRLaunchGroup
|
@@ -52,5 +53,12 @@ module SMARTAppLaunch
|
|
52
53
|
|
53
54
|
redirect_index = children.find_index { |child| child.id.to_s.end_with? 'app_redirect' }
|
54
55
|
children[redirect_index] = children.pop
|
56
|
+
|
57
|
+
test from: :smart_token_exchange_stu2
|
58
|
+
|
59
|
+
token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'token_exchange' }
|
60
|
+
children[token_exchange_index] = children.pop
|
61
|
+
|
62
|
+
children[token_exchange_index].id(:smart_token_exchange)
|
55
63
|
end
|
56
64
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
module SMARTAppLaunch
|
4
|
+
class JWKS
|
5
|
+
class << self
|
6
|
+
def jwks_json
|
7
|
+
@jwks_json ||=
|
8
|
+
JSON.pretty_generate(
|
9
|
+
{ keys: jwks.export[:keys].select { |key| key[:key_ops]&.include?('verify') } }
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def default_jwks_path
|
14
|
+
@default_jwks_path ||= File.join(__dir__, 'smart_jwks.json')
|
15
|
+
end
|
16
|
+
|
17
|
+
def jwks_path
|
18
|
+
@jwks_path ||=
|
19
|
+
ENV.fetch('SMART_JWKS_PATH', default_jwks_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def jwks
|
23
|
+
@jwks ||= JWT::JWK::Set.new(JSON.parse(File.read(jwks_path)))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -2,7 +2,8 @@ module SMARTAppLaunch
|
|
2
2
|
class LaunchReceivedTest < Inferno::Test
|
3
3
|
title 'EHR server sends launch parameter'
|
4
4
|
description %(
|
5
|
-
|
5
|
+
The `launch` URL parameter associates the app's authorization request with
|
6
|
+
the current EHR session.
|
6
7
|
)
|
7
8
|
id :smart_launch_received
|
8
9
|
|
@@ -13,7 +14,7 @@ module SMARTAppLaunch
|
|
13
14
|
launch = request.query_parameters['launch']
|
14
15
|
output launch: launch
|
15
16
|
|
16
|
-
assert launch.present?, 'No `launch`
|
17
|
+
assert launch.present?, 'No `launch` parameter received'
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
{
|
2
|
+
"keys":
|
3
|
+
[{
|
4
|
+
"kty": "EC",
|
5
|
+
"crv": "P-384",
|
6
|
+
"x": "JQKTsV6PT5Szf4QtDA1qrs0EJ1pbimQmM2SKvzOlIAqlph3h1OHmZ2i7MXahIF2C",
|
7
|
+
"y": "bRWWQRJBgDa6CTgwofYrHjVGcO-A7WNEnu4oJA5OUJPPPpczgx1g2NsfinK-D2Rw",
|
8
|
+
"use": "sig",
|
9
|
+
"key_ops": [
|
10
|
+
"verify"
|
11
|
+
],
|
12
|
+
"ext": true,
|
13
|
+
"kid": "4b49a739d1eb115b3225f4cf9beb6d1b",
|
14
|
+
"alg": "ES384"
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"kty": "EC",
|
18
|
+
"crv": "P-384",
|
19
|
+
"d": "kDkn55p7gryKk2tj6z2ij7ExUnhi0ngxXosvqa73y7epwgthFqaJwApmiXXU2yhK",
|
20
|
+
"x": "JQKTsV6PT5Szf4QtDA1qrs0EJ1pbimQmM2SKvzOlIAqlph3h1OHmZ2i7MXahIF2C",
|
21
|
+
"y": "bRWWQRJBgDa6CTgwofYrHjVGcO-A7WNEnu4oJA5OUJPPPpczgx1g2NsfinK-D2Rw",
|
22
|
+
"key_ops": [
|
23
|
+
"sign"
|
24
|
+
],
|
25
|
+
"ext": true,
|
26
|
+
"kid": "4b49a739d1eb115b3225f4cf9beb6d1b",
|
27
|
+
"alg": "ES384"
|
28
|
+
},
|
29
|
+
{
|
30
|
+
"kty": "RSA",
|
31
|
+
"alg": "RS384",
|
32
|
+
"n": "vjbIzTqiY8K8zApeNng5ekNNIxJfXAue9BjoMrZ9Qy9m7yIA-tf6muEupEXWhq70tC7vIGLqJJ4O8m7yiH8H2qklX2mCAMg3xG3nbykY2X7JXtW9P8VIdG0sAMt5aZQnUGCgSS3n0qaooGn2LUlTGIR88Qi-4Nrao9_3Ki3UCiICeCiAE224jGCg0OlQU6qj2gEB3o-DWJFlG_dz1y-Mxo5ivaeM0vWuodjDrp-aiabJcSF_dx26sdC9dZdBKXFDq0t19I9S9AyGpGDJwzGRtWHY6LsskNHLvo8Zb5AsJ9eRZKpnh30SYBZI9WHtzU85M9WQqdScR69Vyp-6Uhfbvw",
|
33
|
+
"e": "AQAB",
|
34
|
+
"use": "sig",
|
35
|
+
"key_ops": [
|
36
|
+
"verify"
|
37
|
+
],
|
38
|
+
"ext": true,
|
39
|
+
"kid": "b41528b6f37a9500edb8a905a595bdd7"
|
40
|
+
},
|
41
|
+
{
|
42
|
+
"kty": "RSA",
|
43
|
+
"alg": "RS384",
|
44
|
+
"n": "vjbIzTqiY8K8zApeNng5ekNNIxJfXAue9BjoMrZ9Qy9m7yIA-tf6muEupEXWhq70tC7vIGLqJJ4O8m7yiH8H2qklX2mCAMg3xG3nbykY2X7JXtW9P8VIdG0sAMt5aZQnUGCgSS3n0qaooGn2LUlTGIR88Qi-4Nrao9_3Ki3UCiICeCiAE224jGCg0OlQU6qj2gEB3o-DWJFlG_dz1y-Mxo5ivaeM0vWuodjDrp-aiabJcSF_dx26sdC9dZdBKXFDq0t19I9S9AyGpGDJwzGRtWHY6LsskNHLvo8Zb5AsJ9eRZKpnh30SYBZI9WHtzU85M9WQqdScR69Vyp-6Uhfbvw",
|
45
|
+
"e": "AQAB",
|
46
|
+
"d": "rriV9GYimi5by7TOW4xNh6_gYBHVRDBsft2OFF8qapdVHt2GNuRDDxc_B6ga6TY2Enh2MLKLTr1dD3W4FIdTCJiMerrorp07FJS7nJEMgWQDxrfgkX4_EqrhW42L5d4vypYnRXEEW6u4gzkx5uFOkdvJBIK7CsIdSaBFYhochnynNgvbKWasi4rl2hayEH8tdf3B7Z6OIH9alspBTaq3j_zJt_KkrpYEzIUb4UgALB5NTWn5YKr0Avk_asOg8YfjViQwO9ASGaWjQeJ2Rx8OEQwBMQHSDMCSWNiWmYOu9PcwSZFc1vLxqzyIM8QrQSJHCCMo_wGYgke_r0CLeONHEQ",
|
47
|
+
"p": "5hH_QApWGeobRi1n7XbMfJYohB8K3JDPa0MspfplHpJ-17JiGG2sNoBdBcpaPRf9OX48P8VqO0qrSSRAk-I-uO6OO9BHbIukXJILqnY2JmurYzbcYbt5FVbknlHRJojkF6-7sFBazpueUlOnXCw7X7Z_SkfNE4QX5Ejm2Zm5mek",
|
48
|
+
"q": "06bZz7c7K9s1-aEZsxYnLJ9eTpKlt1tIBDA_LwIh5W3w259pes2kUtimbnkyOf-V2ZIERsFCh5s-S9IOEMvAIa6M5j9GW1ILNT7AcHIUfcyFcH-FF8BU_KJdRP5PXnIXFdYcylvsdoIdchy1AaUIzyiKRCU3HBYI75hez0l_F2c",
|
49
|
+
"dp": "h_sVIXW6hCCRND48EedIX06k7conMkxIu_39ErDXOWWeoMAnKIcR5TijQnviL__QxD1vQMXezuKIMHfDz2RGbClbWdD1lhtG7wvG515tDPJQXxia0wzqOQmdoFF9S8hXAAT26vPjaAAkaEZXQaxG_4Au5elgNWu6b0wDXZN1Vpk",
|
50
|
+
"dq": "GqS0YpuUTU8JGmWXUJ4HTGy7eHSpe8134V8ZdRd1oOYYHe2RX64nc25mdR24nuh3uq3Q7_9AGsYGL5E_yAl-JD9O6WUpvDE1y_wcSYty3Os0GRdUb8r8Z9kgmKDS6Pa_xTXw5eBwgfKbNlQ6zPwzgbB-x1lP-K8lbNPni3ybDR0",
|
51
|
+
"qi": "cqQfoi0sM5Su8ZOhznmdWrDIQB28H6fBKiabgaIKkbWZV4e0nwFvLquHjPOvv4Ao8iEGU5dyhvg0n5BKYPi-4mp6M6OA1sy0NrTr7EsKSYGyu2pBq9rw4oAYTM2LXKg6K-awgUUlkc451IwxHBAe15PWCBM3kvLQeijNid0Vz5I",
|
52
|
+
"key_ops": [
|
53
|
+
"sign"
|
54
|
+
],
|
55
|
+
"ext": true,
|
56
|
+
"kid": "b41528b6f37a9500edb8a905a595bdd7"
|
57
|
+
}]
|
58
|
+
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'tls_test_kit'
|
2
2
|
|
3
|
+
require_relative 'jwks'
|
3
4
|
require_relative 'version'
|
4
5
|
require_relative 'discovery_stu1_group'
|
5
6
|
require_relative 'standalone_launch_group'
|
@@ -21,15 +22,39 @@ module SMARTAppLaunch
|
|
21
22
|
request.query_parameters['state']
|
22
23
|
end
|
23
24
|
|
25
|
+
route(
|
26
|
+
:get,
|
27
|
+
'/.well-known/jwks.json',
|
28
|
+
->(_env) { [200, { 'Content-Type' => 'application/json' }, [JWKS.jwks_json]] }
|
29
|
+
)
|
30
|
+
|
24
31
|
config options: {
|
25
32
|
redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect",
|
26
33
|
launch_uri: "#{Inferno::Application['base_url']}/custom/smart/launch"
|
27
34
|
}
|
28
35
|
|
36
|
+
description <<~DESCRIPTION
|
37
|
+
The SMART App Launch Test Suite verifies that systems correctly implement
|
38
|
+
the [SMART App Launch IG](http://hl7.org/fhir/smart-app-launch/1.0.0/)
|
39
|
+
for providing authorization and/or authentication services to client
|
40
|
+
applications accessing HL7® FHIR® APIs. To get started, please first register
|
41
|
+
the Inferno client as a SMART App with the following information:
|
42
|
+
|
43
|
+
* SMART Launch URI: `#{config.options[:launch_uri]}`
|
44
|
+
* OAuth Redirect URI: `#{config.options[:redirect_uri]}`
|
45
|
+
DESCRIPTION
|
46
|
+
|
29
47
|
group do
|
30
48
|
title 'Standalone Launch'
|
31
49
|
id :smart_full_standalone_launch
|
32
50
|
|
51
|
+
input_instructions <<~INSTRUCTIONS
|
52
|
+
Please register the Inferno client as a SMART App with the following
|
53
|
+
information:
|
54
|
+
|
55
|
+
* OAuth Redirect URI: `#{config.options[:redirect_uri]}`
|
56
|
+
INSTRUCTIONS
|
57
|
+
|
33
58
|
run_as_group
|
34
59
|
|
35
60
|
group from: :smart_discovery
|
@@ -92,6 +117,14 @@ module SMARTAppLaunch
|
|
92
117
|
title 'EHR Launch'
|
93
118
|
id :smart_full_ehr_launch
|
94
119
|
|
120
|
+
input_instructions <<~INSTRUCTIONS
|
121
|
+
Please register the Inferno client as a SMART App with the following
|
122
|
+
information:
|
123
|
+
|
124
|
+
* SMART Launch URI: `#{config.options[:launch_uri]}`
|
125
|
+
* OAuth Redirect URI: `#{config.options[:redirect_uri]}`
|
126
|
+
INSTRUCTIONS
|
127
|
+
|
95
128
|
run_as_group
|
96
129
|
|
97
130
|
group from: :smart_discovery
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'tls_test_kit'
|
2
2
|
|
3
|
+
require_relative 'jwks'
|
3
4
|
require_relative 'version'
|
4
5
|
require_relative 'discovery_stu2_group'
|
5
6
|
require_relative 'standalone_launch_group_stu2'
|
@@ -21,6 +22,12 @@ module SMARTAppLaunch
|
|
21
22
|
request.query_parameters['state']
|
22
23
|
end
|
23
24
|
|
25
|
+
route(
|
26
|
+
:get,
|
27
|
+
'/.well-known/jwks.json',
|
28
|
+
->(_env) { [200, { 'Content-Type' => 'application/json' }, [JWKS.jwks_json]] }
|
29
|
+
)
|
30
|
+
|
24
31
|
@post_auth_page = File.read(File.join(__dir__, 'post_auth.html'))
|
25
32
|
post_auth_handler = proc { [200, {}, [@post_auth_page]] }
|
26
33
|
|
@@ -32,10 +39,38 @@ module SMARTAppLaunch
|
|
32
39
|
post_authorization_uri: "#{Inferno::Application['base_url']}/custom/smart_stu2/post_auth"
|
33
40
|
}
|
34
41
|
|
42
|
+
description <<~DESCRIPTION
|
43
|
+
The SMART App Launch Test Suite verifies that systems correctly implement
|
44
|
+
the [SMART App Launch IG](http://hl7.org/fhir/smart-app-launch/STU2/)
|
45
|
+
for providing authorization and/or authentication services to client
|
46
|
+
applications accessing HL7® FHIR® APIs. To get started, please first register
|
47
|
+
the Inferno client as a SMART App with the following information:
|
48
|
+
|
49
|
+
* SMART Launch URI: `#{config.options[:launch_uri]}`
|
50
|
+
* OAuth Redirect URI: `#{config.options[:redirect_uri]}`
|
51
|
+
|
52
|
+
If using asymmetric client authentication, register Inferno with the
|
53
|
+
following JWK Set URL:
|
54
|
+
|
55
|
+
* `#{Inferno::Application[:base_url]}/custom/smart_stu2/.well-known/jwks.json`
|
56
|
+
DESCRIPTION
|
57
|
+
|
35
58
|
group do
|
36
59
|
title 'Standalone Launch'
|
37
60
|
id :smart_full_standalone_launch
|
38
61
|
|
62
|
+
input_instructions <<~INSTRUCTIONS
|
63
|
+
Please register the Inferno client as a SMART App with the following
|
64
|
+
information:
|
65
|
+
|
66
|
+
* OAuth Redirect URI: `#{config.options[:redirect_uri]}`
|
67
|
+
|
68
|
+
If using asymmetric client authentication, register Inferno with the
|
69
|
+
following JWK Set URL:
|
70
|
+
|
71
|
+
* `#{Inferno::Application[:base_url]}/custom/smart_stu2/.well-known/jwks.json`
|
72
|
+
INSTRUCTIONS
|
73
|
+
|
39
74
|
run_as_group
|
40
75
|
|
41
76
|
group from: :smart_discovery_stu2
|
@@ -98,6 +133,19 @@ module SMARTAppLaunch
|
|
98
133
|
title 'EHR Launch'
|
99
134
|
id :smart_full_ehr_launch
|
100
135
|
|
136
|
+
input_instructions <<~INSTRUCTIONS
|
137
|
+
Please register the Inferno client as a SMART App with the following
|
138
|
+
information:
|
139
|
+
|
140
|
+
* SMART Launch URI: `#{config.options[:launch_uri]}`
|
141
|
+
* OAuth Redirect URI: `#{config.options[:redirect_uri]}`
|
142
|
+
|
143
|
+
If using asymmetric client authentication, register Inferno with the
|
144
|
+
following JWK Set URL:
|
145
|
+
|
146
|
+
* `#{Inferno::Application[:base_url]}/custom/smart_stu2/.well-known/jwks.json`
|
147
|
+
INSTRUCTIONS
|
148
|
+
|
101
149
|
run_as_group
|
102
150
|
|
103
151
|
group from: :smart_discovery_stu2
|
@@ -44,7 +44,8 @@ module SMARTAppLaunch
|
|
44
44
|
client_secret: {
|
45
45
|
name: :standalone_client_secret,
|
46
46
|
title: 'Standalone Client Secret',
|
47
|
-
description: 'Client Secret provided during registration of Inferno as a standalone application'
|
47
|
+
description: 'Client Secret provided during registration of Inferno as a standalone application. ' \
|
48
|
+
'Only for clients using confidential symmetric authentication.'
|
48
49
|
},
|
49
50
|
requested_scopes: {
|
50
51
|
name: :standalone_requested_scopes,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'app_redirect_test_stu2'
|
2
|
+
require_relative 'token_exchange_stu2_test'
|
2
3
|
require_relative 'standalone_launch_group'
|
3
4
|
|
4
5
|
module SMARTAppLaunch
|
@@ -48,5 +49,12 @@ module SMARTAppLaunch
|
|
48
49
|
|
49
50
|
redirect_index = children.find_index { |child| child.id.to_s.end_with? 'app_redirect' }
|
50
51
|
children[redirect_index] = children.pop
|
52
|
+
|
53
|
+
test from: :smart_token_exchange_stu2
|
54
|
+
|
55
|
+
token_exchange_index = children.find_index { |child| child.id.to_s.end_with? 'token_exchange' }
|
56
|
+
children[token_exchange_index] = children.pop
|
57
|
+
|
58
|
+
children[token_exchange_index].id('smart_token_exchange')
|
51
59
|
end
|
52
60
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'client_assertion_builder'
|
2
|
+
require_relative 'token_exchange_test'
|
3
|
+
|
4
|
+
module SMARTAppLaunch
|
5
|
+
class TokenExchangeSTU2Test < TokenExchangeTest
|
6
|
+
title 'OAuth token exchange request succeeds when supplied correct information'
|
7
|
+
description %(
|
8
|
+
After obtaining an authorization code, the app trades the code for an
|
9
|
+
access token via HTTP POST to the EHR authorization server's token
|
10
|
+
endpoint URL, using content-type application/x-www-form-urlencoded, as
|
11
|
+
described in section [4.1.3 of
|
12
|
+
RFC6749](https://tools.ietf.org/html/rfc6749#section-4.1.3).
|
13
|
+
)
|
14
|
+
id :smart_token_exchange_stu2
|
15
|
+
|
16
|
+
input :client_auth_encryption_method,
|
17
|
+
title: 'Encryption Method (Confidential Asymmetric Client Auth Only)',
|
18
|
+
type: 'radio',
|
19
|
+
default: 'ES384',
|
20
|
+
options: {
|
21
|
+
list_options: [
|
22
|
+
{
|
23
|
+
label: 'ES384',
|
24
|
+
value: 'ES384'
|
25
|
+
},
|
26
|
+
{
|
27
|
+
label: 'RS384',
|
28
|
+
value: 'RS384'
|
29
|
+
}
|
30
|
+
]
|
31
|
+
}
|
32
|
+
|
33
|
+
input :client_auth_type,
|
34
|
+
title: 'Client Authentication Method',
|
35
|
+
type: 'radio',
|
36
|
+
options: {
|
37
|
+
list_options: [
|
38
|
+
{
|
39
|
+
label: 'Public',
|
40
|
+
value: 'public'
|
41
|
+
},
|
42
|
+
{
|
43
|
+
label: 'Confidential Symmetric',
|
44
|
+
value: 'confidential_symmetric'
|
45
|
+
},
|
46
|
+
{
|
47
|
+
label: 'Confidential Asymmetric',
|
48
|
+
value: 'confidential_asymmetric'
|
49
|
+
}
|
50
|
+
]
|
51
|
+
}
|
52
|
+
|
53
|
+
config(
|
54
|
+
inputs: {
|
55
|
+
use_pkce: {
|
56
|
+
default: 'true',
|
57
|
+
options: {
|
58
|
+
list_options: [
|
59
|
+
{
|
60
|
+
label: 'Enabled',
|
61
|
+
value: 'true'
|
62
|
+
}
|
63
|
+
]
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
)
|
68
|
+
|
69
|
+
def add_credentials_to_request(oauth2_params, oauth2_headers)
|
70
|
+
if client_auth_type == 'confidential_symmetric'
|
71
|
+
assert client_secret.present?,
|
72
|
+
"A client secret must be provided when using confidential symmetric client authentication."
|
73
|
+
|
74
|
+
client_credentials = "#{client_id}:#{client_secret}"
|
75
|
+
oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
|
76
|
+
elsif client_auth_type == 'public'
|
77
|
+
oauth2_params[:client_id] = client_id
|
78
|
+
else
|
79
|
+
oauth2_params.merge!(
|
80
|
+
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
|
81
|
+
client_assertion: ClientAssertionBuilder.build(
|
82
|
+
iss: client_id,
|
83
|
+
sub: client_id,
|
84
|
+
aud: smart_token_url,
|
85
|
+
client_auth_encryption_method: client_auth_encryption_method
|
86
|
+
)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -38,22 +38,26 @@ module SMARTAppLaunch
|
|
38
38
|
|
39
39
|
config options: { redirect_uri: "#{Inferno::Application['base_url']}/custom/smart/redirect" }
|
40
40
|
|
41
|
+
def add_credentials_to_request(oauth2_params, oauth2_headers)
|
42
|
+
if client_secret.present?
|
43
|
+
client_credentials = "#{client_id}:#{client_secret}"
|
44
|
+
oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
|
45
|
+
else
|
46
|
+
oauth2_params[:client_id] = client_id
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
41
50
|
run do
|
42
51
|
skip_if request.query_parameters['error'].present?, 'Error during authorization request'
|
43
52
|
|
44
53
|
oauth2_params = {
|
45
|
-
grant_type: 'authorization_code',
|
46
54
|
code: code,
|
47
|
-
redirect_uri: config.options[:redirect_uri]
|
55
|
+
redirect_uri: config.options[:redirect_uri],
|
56
|
+
grant_type: 'authorization_code'
|
48
57
|
}
|
49
58
|
oauth2_headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
50
59
|
|
51
|
-
|
52
|
-
client_credentials = "#{client_id}:#{client_secret}"
|
53
|
-
oauth2_headers['Authorization'] = "Basic #{Base64.strict_encode64(client_credentials)}"
|
54
|
-
else
|
55
|
-
oauth2_params[:client_id] = client_id
|
56
|
-
end
|
60
|
+
add_credentials_to_request(oauth2_params, oauth2_headers)
|
57
61
|
|
58
62
|
if use_pkce == 'true'
|
59
63
|
oauth2_params[:code_verifier] = pkce_code_verifier
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_app_launch_test_kit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen MacVicar
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inferno_core
|
@@ -134,11 +134,13 @@ files:
|
|
134
134
|
- lib/smart_app_launch/app_launch_test.rb
|
135
135
|
- lib/smart_app_launch/app_redirect_test.rb
|
136
136
|
- lib/smart_app_launch/app_redirect_test_stu2.rb
|
137
|
+
- lib/smart_app_launch/client_assertion_builder.rb
|
137
138
|
- lib/smart_app_launch/code_received_test.rb
|
138
139
|
- lib/smart_app_launch/discovery_stu1_group.rb
|
139
140
|
- lib/smart_app_launch/discovery_stu2_group.rb
|
140
141
|
- lib/smart_app_launch/ehr_launch_group.rb
|
141
142
|
- lib/smart_app_launch/ehr_launch_group_stu2.rb
|
143
|
+
- lib/smart_app_launch/jwks.rb
|
142
144
|
- lib/smart_app_launch/launch_received_test.rb
|
143
145
|
- lib/smart_app_launch/openid_connect_group.rb
|
144
146
|
- lib/smart_app_launch/openid_decode_id_token_test.rb
|
@@ -149,10 +151,12 @@ files:
|
|
149
151
|
- lib/smart_app_launch/openid_token_header_test.rb
|
150
152
|
- lib/smart_app_launch/openid_token_payload_test.rb
|
151
153
|
- lib/smart_app_launch/post_auth.html
|
154
|
+
- lib/smart_app_launch/smart_jwks.json
|
152
155
|
- lib/smart_app_launch/smart_stu1_suite.rb
|
153
156
|
- lib/smart_app_launch/smart_stu2_suite.rb
|
154
157
|
- lib/smart_app_launch/standalone_launch_group.rb
|
155
158
|
- lib/smart_app_launch/standalone_launch_group_stu2.rb
|
159
|
+
- lib/smart_app_launch/token_exchange_stu2_test.rb
|
156
160
|
- lib/smart_app_launch/token_exchange_test.rb
|
157
161
|
- lib/smart_app_launch/token_payload_validation.rb
|
158
162
|
- lib/smart_app_launch/token_refresh_body_test.rb
|