Skip to content

Commit

Permalink
FI-2247 backend services migration (#59)
Browse files Browse the repository at this point in the history
  • Loading branch information
alisawallace committed Jan 31, 2024
1 parent ad032d4 commit c4340a8
Show file tree
Hide file tree
Showing 20 changed files with 915 additions and 30 deletions.
12 changes: 11 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
smart_app_launch_test_kit (0.4.0)
inferno_core (>= 0.4.2)
json-jwt (~> 1.15.3)
jwt (~> 2.6)
tls_test_kit (~> 0.2.0)

Expand All @@ -17,9 +18,11 @@ GEM
zeitwerk (~> 2.3)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
base62-rb (0.3.1)
bcp47 (0.3.3)
i18n
bindata (2.4.15)
blueprinter (0.25.2)
byebug (11.1.3)
coderay (1.1.3)
Expand Down Expand Up @@ -141,6 +144,7 @@ GEM
http-accept (1.7.0)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
Expand Down Expand Up @@ -172,6 +176,11 @@ GEM
io-console (0.5.11)
irb (1.4.2)
reline (>= 0.3.0)
json-jwt (1.15.3)
activesupport (>= 4.2)
aes_key_wrap
bindata
httpclient
jwt (2.7.1)
kramdown (2.4.0)
rexml
Expand Down Expand Up @@ -282,6 +291,7 @@ GEM
PLATFORMS
arm64-darwin-21
arm64-darwin-22
arm64-darwin-23
x86_64-darwin-20
x86_64-darwin-22
x86_64-linux
Expand All @@ -296,4 +306,4 @@ DEPENDENCIES
webmock (~> 3.11)

BUNDLED WITH
2.3.23
2.5.3
5 changes: 5 additions & 0 deletions config/presets/inferno_reference_server_stu2_preset.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
}
]
}
},
{
"name": "backend_services_client_id",
"type": "text",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJlZ2lzdHJhdGlvbi10b2tlbiJ9.eyJqd2tzX3VybCI6Imh0dHA6Ly8xMC4xNS4yNTIuNzMvaW5mZXJuby8ud2VsbC1rbm93bi9qd2tzLmpzb24iLCJhY2Nlc3NUb2tlbnNFeHBpcmVJbiI6MTUsImlhdCI6MTU5NzQxMzE5NX0.q4v4Msc74kN506KTZ0q_minyapJw0gwlT6M_uiL73S4"
}
]
}
88 changes: 88 additions & 0 deletions lib/smart_app_launch/backend_services_authorization_group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require_relative 'backend_services_authorization_request_builder'
require_relative 'backend_services_invalid_grant_type_test'
require_relative 'backend_services_invalid_client_assertion_test'
require_relative 'backend_services_invalid_jwt_test'
require_relative 'backend_services_authorization_request_success_test'
require_relative 'backend_services_authorization_response_body_test'
require_relative 'token_exchange_stu2_test'

module SMARTAppLaunch
class BackendServicesAuthorizationGroup < Inferno::TestGroup
title 'SMART Backend Services Authorization'
short_description 'Demonstrate SMART Backend Services Authorization'

id :backend_services_authorization

input :smart_token_url,
title: 'Backend Services Token Endpoint',
description: <<~DESCRIPTION
The OAuth 2.0 Token Endpoint used by the Backend Services specification to provide bearer tokens.
DESCRIPTION

input :backend_services_client_id,
title: 'Backend Services Client ID',
description: 'Client ID provided at registration to the Inferno application.'
input :backend_services_requested_scope,
title: 'Backend Services Requested Scopes',
description: 'Backend Services Scopes provided at registration to the Inferno application; will be `system/` scopes',
default: 'system/*.read'

input :client_auth_encryption_method,
title: 'Encryption Method for Asymmetric Confidential Client Authorization',
description: <<~DESCRIPTION,
The server is required to suport either ES384 or RS384 encryption methods for JWT signature verification.
Select which method to use.
DESCRIPTION
type: 'radio',
default: 'ES384',
options: {
list_options: [
{
label: 'ES384',
value: 'ES384'
},
{
label: 'RS384',
value: 'RS384'
}
]
}
input :backend_services_jwks_kid,
title: 'Backend Services JWKS kid',
description: <<~DESCRIPTION,
The key ID of the JWKS private key to use for signing the client assertion when fetching an auth token.
Defaults to the first JWK in the list if no kid is supplied.
DESCRIPTION
optional: true

output :bearer_token

test from: :tls_version_test do
title 'Authorization service token endpoint secured by transport layer security'
description <<~DESCRIPTION
The [SMART App Launch 2.0.0 IG specification for Backend Services](https://hl7.org/fhir/smart-app-launch/STU2/backend-services.html#request-1)
states "the client SHALL use the Transport Layer Security (TLS) Protocol Version 1.2 (RFC5246)
or a more recent version of TLS to authenticate the identity of the FHIR authorization server and to
establish an encrypted, integrity-protected link for securing all exchanges between the client and the
FHIR authorization server’s token endpoint. All exchanges described herein between the client and the
FHIR server SHALL be secured using TLS V1.2 or a more recent version of TLS."
DESCRIPTION
id :smart_backend_services_token_tls_version

config(
inputs: { url: { name: :smart_token_url } },
options: { minimum_allowed_version: OpenSSL::SSL::TLS1_2_VERSION }
)
end

test from: :smart_backend_services_invalid_grant_type

test from: :smart_backend_services_invalid_client_assertion

test from: :smart_backend_services_invalid_jwt

test from: :smart_backend_services_auth_request_success

test from: :smart_backend_services_auth_response_body
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require 'json/jwt'
require_relative 'client_assertion_builder'

module SMARTAppLaunch
class BackendServicesAuthorizationRequestBuilder
def self.build(...)
new(...).authorization_request
end

attr_reader :encryption_method, :scope, :iss, :sub, :aud, :content_type, :grant_type, :client_assertion_type, :exp,
:jti, :kid

def initialize(
encryption_method:,
scope:,
iss:,
sub:,
aud:,
content_type: 'application/x-www-form-urlencoded',
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
exp: 5.minutes.from_now,
jti: SecureRandom.hex(32),
kid: nil
)
@encryption_method = encryption_method
@scope = scope
@iss = iss
@sub = sub
@aud = aud
@content_type = content_type
@grant_type = grant_type
@client_assertion_type = client_assertion_type
@exp = exp
@jti = jti
@kid = kid
end

def authorization_request_headers
{
content_type:,
accept: 'application/json'
}.compact
end

def authorization_request_query_values
{
'scope' => scope,
'grant_type' => grant_type,
'client_assertion_type' => client_assertion_type,
'client_assertion' => client_assertion.to_s
}.compact
end

def client_assertion
@client_assertion ||= ClientAssertionBuilder.build(
client_auth_encryption_method: encryption_method,
iss: iss,
sub: sub,
aud: aud,
exp: exp.to_i,
jti: jti,
kid: kid
)
end

def authorization_request
uri = Addressable::URI.new
uri.query_values = authorization_request_query_values

{ body: uri.query, headers: authorization_request_headers }
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require_relative 'backend_services_authorization_request_builder'
require_relative 'backend_services_authorization_group'

module SMARTAppLaunch
class BackendServicesAuthorizationRequestSuccessTest < Inferno::Test
id :smart_backend_services_auth_request_success
title 'Authorization request succeeds when supplied correct information'
description <<~DESCRIPTION
The [SMART App Launch 2.0.0 IG specification for Backend Services](https://hl7.org/fhir/smart-app-launch/STU2/backend-services.html#issue-access-token)
states "If the access token request is valid and authorized, the authorization server SHALL issue an access token in response."
DESCRIPTION

input :client_auth_encryption_method,
:backend_services_requested_scope,
:backend_services_client_id,
:smart_token_url,
:backend_services_jwks_kid

output :authentication_response

http_client :token_endpoint do
url :smart_token_url
end

run do
post_request_content = BackendServicesAuthorizationRequestBuilder.build(encryption_method: client_auth_encryption_method,
scope: backend_services_requested_scope,
iss: backend_services_client_id,
sub: backend_services_client_id,
aud: smart_token_url,
kid: backend_services_jwks_kid)

authentication_response = post(**{ client: :token_endpoint }.merge(post_request_content))

assert_response_status([200, 201])

output authentication_response: authentication_response.response_body
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require_relative 'backend_services_authorization_request_builder'

module SMARTAppLaunch
class BackendServicesAuthorizationResponseBodyTest < Inferno::Test
id :smart_backend_services_auth_response_body
title 'Authorization request response body contains required information encoded in JSON'
description <<~DESCRIPTION
The [SMART App Launch 2.0.0 IG specification for Backend Services](https://hl7.org/fhir/smart-app-launch/STU2/backend-services.html#issue-access-token)
states The access token response SHALL be a JSON object with the following properties:
| Token Property | Required? | Description |
| --- | --- | --- |
| `access_token` | required | The access token issued by the authorization server. |
| `token_type` | required | Fixed value: `bearer`. |
| `expires_in` | required | The lifetime in seconds of the access token. The recommended value is `300`, for a five-minute token lifetime. |
| `scope` | required | Scope of access authorized. Note that this can be different from the scopes requested by the app. |
DESCRIPTION

input :authentication_response
output :bearer_token

run do
skip_if authentication_response.blank?, 'No authentication response received.'

assert_valid_json(authentication_response)
response_body = JSON.parse(authentication_response)

access_token = response_body['access_token']
assert access_token.present?, 'Token response did not contain access_token as required'

output bearer_token: access_token

required_keys = ['token_type', 'expires_in', 'scope']

required_keys.each do |key|
assert response_body[key].present?, "Token response did not contain #{key} as required"
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require_relative 'backend_services_authorization_request_builder'

module SMARTAppLaunch
class BackendServicesInvalidClientAssertionTest < Inferno::Test
id :smart_backend_services_invalid_client_assertion
title 'Authorization request fails when supplied invalid client_assertion_type'
description <<~DESCRIPTION
The [SMART App Launch 2.0.0 IG specification for Backend Services](https://hl7.org/fhir/smart-app-launch/STU2/backend-services.html#request-1)
defines the required fields for the authorization request, made via HTTP POST to authorization
token endpoint.
This includes the `client_assertion_type` parameter, where the value must be `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`.
The [OAuth 2.0 Authorization Framework Section 4.3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3.3)
describes the proper response for an invalid request in the client credentials grant flow:
"If the request failed client authentication or is invalid, the authorization server returns an
error response as described in [Section 5.2](https://tools.ietf.org/html/rfc6749#section-5.2)."
DESCRIPTION

input :client_auth_encryption_method,
:backend_services_requested_scope,
:backend_services_client_id,
:smart_token_url,
:backend_services_jwks_kid

http_client :token_endpoint do
url :smart_token_url
end

run do
post_request_content = BackendServicesAuthorizationRequestBuilder.build(encryption_method: client_auth_encryption_method,
scope: backend_services_requested_scope,
iss: backend_services_client_id,
sub: backend_services_client_id,
aud: smart_token_url,
client_assertion_type: 'not_an_assertion_type',
kid: backend_services_jwks_kid)

post(**{ client: :token_endpoint }.merge(post_request_content))

assert_response_status(400)
end
end
end
Loading

0 comments on commit c4340a8

Please sign in to comment.