Skip to content

Commit 84ead00

Browse files
authored
Follow the specs more closely, switch to JWTs.jl, and fix the tests (#9)
* Replace JSONWebTokens.jl with JWTs.jl * Follow the specs more closely and fix tests * Update Project.toml * Fix Documenter * Fix typo * Add `/metadata` fallback * Additional test * Extend tests * Make keyid mandatory * Update public_jwkset.json * Simplify tests
1 parent 465e6ad commit 84ead00

13 files changed

+222
-121
lines changed

Project.toml

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
name = "SMARTBackendServices"
22
uuid = "78af60b6-7677-4c75-8291-bd270d1b4390"
33
authors = ["Dilum Aluthge", "contributors"]
4-
version = "1.0.1"
4+
version = "2.0.0"
55

66
[deps]
77
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
88
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
99
HealthBase = "94e1309d-ccf4-42de-905f-515f1d7b1cae"
1010
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
11-
JSONWebTokens = "9b8beb19-0777-58c6-920b-28f749fee4d3"
11+
JWTs = "d850fbd6-035d-5a70-a269-1ca2e636ac6c"
1212
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
13-
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
1413
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
1514

1615
[compat]
1716
HTTP = "0.9.3"
1817
HealthBase = "1.0.1"
1918
JSON3 = "1.5.1"
20-
JSONWebTokens = "0.3.4, 1"
21-
TimeZones = "1.5.3"
19+
JWTs = "0.2.4"
2220
URIs = "1.2"
2321
julia = "1.5"
2422

2523
[extras]
26-
JSONWebTokens = "9b8beb19-0777-58c6-920b-28f749fee4d3"
24+
MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d"
2725
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2826

2927
[targets]
30-
test = ["JSONWebTokens", "Test"]
28+
test = ["MbedTLS", "Test"]

docs/Project.toml

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[deps]
22
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
33
SMARTBackendServices = "78af60b6-7677-4c75-8291-bd270d1b4390"
4+
5+
[compat]
6+
Documenter = "1"

docs/make.jl

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ makedocs(;
1515
"Home" => "index.md",
1616
"API" => "api.md",
1717
],
18-
strict=true,
1918
)
2019

2120
deploydocs(;

src/SMARTBackendServices.jl

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import Dates
44
import HTTP
55
import HealthBase
66
import JSON3
7-
import JSONWebTokens
7+
import JWTs
88
import Random
9-
import TimeZones
109
import URIs
1110

1211
const get_fhir_access_token = HealthBase.get_fhir_access_token
@@ -19,6 +18,5 @@ include("types.jl")
1918

2019
include("backend_services.jl")
2120
include("jwt.jl")
22-
include("timestamps.jl")
2321

2422
end # module

src/backend_services.jl

+124-18
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,151 @@
1-
function _backend_services_create_jwt(config::BackendServicesConfig)
1+
function _backend_services_create_jwt(config::BackendServicesConfig, token_endpoint::AbstractString)
2+
# Random string that uniquely identifies the JWT
23
jti = Random.randstring(150)
34

4-
now = TimeZones.now(TimeZones.localzone())
5-
expiration_time = now + Dates.Minute(4)
6-
expiration_time_seconds_since_epoch_utc = integer_seconds_since_the_epoch_utc(expiration_time)
7-
5+
# Expiration time (integer) in seconds since "epoch"
6+
# SHALL be no more than 5 minutes in the future
7+
expiration_time = Dates.now(Dates.UTC) + Dates.Minute(4)
8+
expiration_time_seconds_since_epoch_utc = round(Int, Dates.datetime2unix(expiration_time))
9+
810
jwt_payload_claims_dict = Dict(
9-
"iss" => config.iss,
10-
"sub" => config.sub,
11-
"aud" => config.token_endpoint,
11+
"iss" => config.client_id,
12+
"sub" => config.client_id,
13+
"aud" => token_endpoint,
1214
"jti" => jti,
1315
"exp" => expiration_time_seconds_since_epoch_utc,
1416
)
15-
jwt = JSONWebTokens.encode(config.private_key, jwt_payload_claims_dict)
17+
jwt = JWTs.JWT(; payload = jwt_payload_claims_dict)
18+
19+
# Sign
20+
JWTs.sign!(jwt, config.key, config.keyid)
21+
@assert JWTs.issigned(jwt)
22+
@assert JWTs.kid(jwt) == config.keyid
1623

17-
return jwt
24+
return string(jwt)
1825
end
1926

27+
# Obtain the token endpoint from the well-known URIs
28+
# Ref: https://www.hl7.org/fhir/smart-app-launch/backend-services.html#retrieve-well-knownsmart-configuration
29+
function _token_endpoint_wellknown(config::BackendServicesConfig)
30+
# Request the SMART configuration file
31+
_config_response = HTTP.request(
32+
"GET",
33+
joinpath(config.base_url, ".well-known/smart-configuration");
34+
# In principle, it should be possible to omit the header
35+
# (and servers may ignore it anyway)
36+
# Ref: https://www.hl7.org/fhir/smart-app-launch/conformance.html#using-well-known
37+
headers = ("Accept" => "application/json",),
38+
# Old servers might still only support the /metadata endpoint (even though its use for SMART capabilities is deprecated)
39+
# Hence we do not throw an exception if the request fails but try the /metadata endpoint first
40+
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#declaring-support-for-oauth2-endpoints
41+
# Ref: https://www.hl7.org/fhir/smart-app-launch/conformance.html#smart-on-fhir-oauth-authorization-endpoints-and-capabilities
42+
status_exception = false,
43+
)
44+
45+
# Exit gracefully (return `nothing`) if the server does not convey its SMART capabilities using well-known URIs
46+
if _config_response.status != 200
47+
return nothing
48+
end
49+
50+
# Extract the token endpoint from the JSON response
51+
config_response = JSON3.read(_config_response.body)
52+
get(config_response, :token_endpoint) do
53+
error(
54+
"SMART configuration: Violation of the FHIR specification. The mandatory `token_endpoint` is missing from the Well-Known Uniform Resource Identifiers (URIs) JSON document.",
55+
)
56+
end::String
57+
end
58+
59+
# Obtain the token endpoint from the CapabilityStatement at the /metadata endpoint
60+
# Note: Declaring SMART capabilities using the /metadata endpoint is deprecated but old servers might still not support the well-known URIs
61+
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#declaring-support-for-oauth2-endpoints
62+
function _token_endpoint_metadata(config::BackendServicesConfig)
63+
# Request the CapabilityStatement
64+
_metadata_response = HTTP.request(
65+
"GET",
66+
joinpath(config.base_url, "metadata");
67+
# We only support FHIR version R4
68+
# Ref: https://hl7.org/fhir/R4/versioning.html#mt-version
69+
headers = ("Accept" => "application/fhir+json; fhirVersion=4.0"),
70+
# We throw our own, hopefully more descriptive, exception if necessary
71+
status_exception = false,
72+
)
73+
74+
# Exit gracefully (return `nothing`) if the server does not convey its SMART capabilities at the /metadata endpoint
75+
if _metadata_response.status != 200
76+
return nothing
77+
end
78+
79+
# Extract the token endpoint from the JSON response
80+
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#declaring-support-for-oauth2-endpoints
81+
# Ref: https://hl7.org/fhir/R4/capabilitystatement.html
82+
compat_statement = JSON3.read(_metadata_response.body)
83+
rest = get(compat_statement, :rest, nothing)
84+
if rest !== nothing
85+
for rest in compat_statement.rest
86+
security = get(rest, :security, nothing)
87+
if security !== nothing
88+
extensions = get(security, :extension, nothing)
89+
if extensions !== nothing
90+
for extension in extensions
91+
if get(extension, :url, nothing) ===
92+
"http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"
93+
for url_value in extension.extension
94+
if url_value.url === "token"
95+
return url_value.valueUri::String
96+
end
97+
end
98+
end
99+
end
100+
end
101+
end
102+
end
103+
end
104+
105+
error(
106+
"SMART configuration: Violation of the FHIR specification. The mandatory `token` url of the OAuth2 token endpoint is missing from the FHIR CompatibilityStatement.",
107+
)
108+
end
109+
110+
# Ref: https://www.hl7.org/fhir/smart-app-launch/backend-services.html
20111
"""
21112
backend_services(config::BackendServicesConfig)
22113
"""
23114
function backend_services(config::BackendServicesConfig)
24-
jwt = _backend_services_create_jwt(config)
115+
# Obtain the token endpoint: Try first the well-known URI and then the /metadata endpoint (deprecated)
116+
# On Julia >= 1.7 this can be simplified to
117+
# token_endpoint = @something _token_endpoint_wellknown(config) _token_endpoint_metadata(config) error("...")
118+
token_endpoint = _token_endpoint_wellknown(config)
119+
if token_endpoint === nothing
120+
token_endpoint = _token_endpoint_metadata(config)
121+
if token_endpoint === nothing
122+
# Ref: https://www.hl7.org/fhir/smart-app-launch/conformance.html#smart-on-fhir-oauth-authorization-endpoints-and-capabilities
123+
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#smart-on-fhir-oauth-authorization-endpoints
124+
error(
125+
"SMART configuration: Violation of the FHIR specification. The FHIR server does neither convey its SMART capabilities using a Well-Known Uniform Resource Identifiers (URIs) JSON file nor its CapabilityStatement.",
126+
)
127+
end
128+
end
129+
130+
# Obtain the access token
131+
# Ref: https://www.hl7.org/fhir/smart-app-launch/backend-services.html#obtain-access-token
132+
# Create JWT
133+
jwt = _backend_services_create_jwt(config, token_endpoint)
25134

26135
body_params = Dict{String, String}()
27136
body_params["grant_type"] = "client_credentials"
28137
body_params["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
29138
body_params["client_assertion"] = jwt
30-
31-
if config.scope !== nothing
32-
body_params["scope"] = config.scope
33-
end
139+
body_params["scope"] = config.scope
34140

35141
_response = HTTP.request(
36142
"POST",
37-
config.token_endpoint;
38-
headers = Dict("Content-Type" => "application/x-www-form-urlencoded"),
143+
token_endpoint;
144+
headers = ("Content-Type" => "application/x-www-form-urlencoded",),
39145
body = URIs.escapeuri(body_params),
40146
)
41147

42-
access_token_response = JSON3.read(String(_response.body))
148+
access_token_response = JSON3.read(_response.body)
43149
access_token = access_token_response.access_token
44150

45151
access_token_is_jwt, access_token_jwt_decoded = try_decode_jwt(access_token)

src/jwt.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
function try_decode_jwt(contents::AbstractString)
22
try
3-
jwt_decoded = JSONWebTokens.decode(JSONWebTokens.None(), contents)
3+
jwt_decoded = JWTs.claims(JWTs.JWT(; jwt = contents))
44
return true, jwt_decoded
55
catch
66
end

src/timestamps.jl

-23
This file was deleted.

src/types.jl

+12-14
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
"""
2-
BackendServicesConfig{PK}(; kwargs...)
2+
BackendServicesConfig{T <: JWTs.JWK}(; kwargs...)
33
44
## Required Keyword Arguments:
5-
- `iss::String`
6-
- `private_key::PK`
7-
- `sub::String`
8-
- `token_endpoint::String`
9-
10-
## Optional Keyword Arguments:
11-
- `scope::Union{String, Nothing}`. Default value: `nothing`.
5+
- `base_url`::String
6+
- `client_id::String`
7+
- `scope::String`
8+
- `key::T`
9+
- `keyid::String`
1210
"""
13-
Base.@kwdef struct BackendServicesConfig{PK <: JSONWebTokens.Encoding}
14-
iss::String
15-
private_key::PK
16-
scope::Union{String, Nothing} = nothing
17-
sub::String
18-
token_endpoint::String
11+
Base.@kwdef struct BackendServicesConfig{T <: JWTs.JWK}
12+
base_url::String
13+
client_id::String
14+
scope::String
15+
key::T
16+
keyid::String
1917
end
2018

2119
Base.@kwdef struct BackendServicesResult

test/basic.jl

+46-12
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,55 @@
1-
token_endpoint = "https://launch.smarthealthit.org/v/r4/auth/token"
1+
# This test uses the public https://launch.smarthealthit.org test server
2+
#
3+
# In the webinterface, select "Launch Type": "Backend Service"
4+
# and then switch to the "Client Registration & Validation" tab
5+
#
6+
# There you can register a client (with randomly generated ID),
7+
# possibly restricted to some scope,
8+
# with a JWK set of public keys for authentication.
9+
#
10+
# Use the base URL at the bottom of the page to connect to the
11+
# server with the stated client ID, scope, and keys.
12+
#
13+
# A private key together with a public JWK set can be generated
14+
# e.g. with https://mkjwk.org/ (alternatively, you can e.g.
15+
# generate the key with openssl and create the JWK set manually):
16+
# 1. Select "Key Use": "Signature"
17+
# 2. Select "Algorithm": "RS384"
18+
# 3. Specify a key id (or let it be generated automatically)
19+
# 4. Check "Show X.509"
20+
# 5. Press "Generate"
21+
# 6. Update the `keyid` below
22+
# 7. Save the private key (in X.509 format) as ./key/private.pem
223

3-
client_id = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwdWJfa2V5IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVsSlFrbHFRVTVDWjJ0eGFHdHBSemwzTUVKQlVVVkdRVUZQUTBGUk9FRk5TVWxDUTJkTFEwRlJSVUYzWVdvMVoza3hkRXRvVGtOWVdYTjNOV0YzVkFwd1p5OVRja2xuYm1sU1YybElVVmwyZDFseWFrSk5WSHBYUkhkcGQyRTNXbkZLTDNSalRFTk5lR1Y1T0dRMlRHdDRWbkpoYldOb1lqWkdSMnhaZERaUkNtRnZNbkpRWlRSNGJVZ3hkak4zZW1kbVZqaEljbTFUTTI5R2NqbDRjRFJPTm5rNGNtdFdkekZ2Vmtoc2RqZHpVRTV0VlRkell5OHhhU3RJY1ZOUlRFb0thM3BWY1dOQ2FubzFVME14YkhwMlpYaG5jVzkxWjNKNGRUVk5abWwwTmtGd1pHRjFSVGc0U3k5dVNGVk9TM1l2T1ROWmFqTkNaM3BNSzBGV1UwUkpRUW92Ynpsc2VFVlplVmxHV1RBek5HaFJSVmhwVFVFME4yY3ZVRk5ZU20xU2NHWkRXV2hhVUc4MFNtTkdjRXBoU0V4amVGbGhiRmxVZUdSdVZDODVlREJuQ21sQlJETnJjMlZaY20wNFprd3JjRU5EY1V4bFdHbEZXVm94Y0d0R1pqWjFjMkZ5WVZScVMyeGlaSGxMWjJadEwyNWtWemR5V2xkemJVSkZSVVVyUWtVS01GRkpSRUZSUVVJS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSIsImlzcyI6Imh0dHBzOi8vd2hhdGV2ZXIuc21hcnQvb3VyLXNhbXBsZS1iYWNrZW5kLXNlcnZpY2UiLCJhY2Nlc3NUb2tlbnNFeHBpcmVJbiI6MSwiaWF0IjoxNTEwNzY2MTQzfQ.7YooXIb64Y3_j38n-Gqwa1PqXc-hz-4xJAJF5oqxJVo"
24+
# Settings of the registered client
25+
base_url = "https://launch.smarthealthit.org/v/r4/sim/WzQsIiIsIiIsIiIsMCwwLDAsInN5c3RlbS8qLnJzIiwiIiwiM240M3JpV29zZjdnZUJrWkJ1eWJndkgzTVo3WEhDbnNRMnc2MFRJbDUxa3ZDb2hoeXBWaTc2R0tqUGNZWnFlS3d3NXhnaGZrWE9OOUNXWmUyNGRjRDUxdmdGc3hRcFd5S3UyMzF1YXplcjk1NjJSQVV2VEZZOVFtc3dhZDMzenZYb21OVDlXcEFmTnZ0TmN2aE96T3dkclpaMW1ZQ1Y1cEhmWllZWEpIdlB6N21uR3Zybm52SFVXMVZBTWF5WmYzOGRNOGNZM003djVoaGIyT1hhYmd0WEJUYWJ4OEhvMnJHT2NhS1pHY0RwZHM4a2ZjYUlmREpJb0pENkY3Mk1DUzJQcEI4VW9NMnRzSkZBTk13VUNpVEVvTW5sdXUzSmJQT2tmeTVIdmNlcG1YYVZmQzVQZXlGN0xMVEhnOGVQVFAxV1FYT0R1ZHQ4YmllcFVtZHN2OWVUM0ptUFlJdzRLQ25HbEx2TzhXRktyUXF3ejE4S080RGpTY0hJTFVqUXpEZkdHampYQlVaNXY2bUtvSHV2RXlJMWlqQkNQcDROdlAyOWNFVVFqY3hISTIyU0tua1ZmTFhFSzd3MW1kUmx2akFGT3VNdkpvRVJNYjlIZzYzT1AzaUdCbjExMnIwWFVoSGpHdXpFYjloTmM2M2trVVhJSGtEcUQxUEthVHhvUnExYWZHc3RhNEl0cjM2bUpRVDRPd2N3ZWxLdVgyRjMyZ3VQem92R0E1d0liRzVJNmlvcHA2YTdpbkllSWdnODR3SDVEVlB3UkdyNVRyNEdCdHhwaHRuU3I5dFd6REEwbm9YOVZoRjRBZWhZTWRHMnh0YlZHbWtFUlJzMFBLR0hwUVVZWFo3WURreGt6S3dudmVvazRsSlM3M2ZSaXRXY2dCWmkxVGFWV1pQT1ZzMnJsWFZEVzU3azg3aDFyemFvZmd2WUZwZkdPZGZIbVA4N28yVlhBUThYWW5XOFRIb0x4d2NkSUNuVjltWTB4WjhZbnVLUjM4WHZFaVBseGF4NVpGNzdZUTVzUGJGeTUzUnU2Q2kyRGx5NnZVOXF1dVRWSzNNSmFRSTFJVWZScUxRRndVaXZ0WGw3aUZESDBtUFdRVXM1c2tSelhLUUVjbkxWUjJmVTBUVnQzOU1YNVduU1Bnb2VOVnd4dDg0aHNzZU9LYkZKelptR1hTalNjYkZMRHFVTjNES0gwN0tIS09zMGVNSVlHdzBkbm5sdmpsUU00TlRsZ1R5b2dJZmdCd0xmQ2VYdVRkRUhCWGtJbU5DRERxTktzeFZTOWlyVkNXdlpjRFR4anJZN250MWZQVzNXb0dmR1ZqeG0ycTVhbzcxYm1NSElyeEh1dU93azFHMUMxeiIsIiIsIiIsIiIsIntcImtleXNcIjogW3tcbiAgICBcImt0eVwiOiBcIlJTQVwiLFxuICAgIFwiZVwiOiBcIkFRQUJcIixcbiAgICBcInVzZVwiOiBcInNpZ1wiLFxuICAgIFwia2lkXCI6IFwiWWIwOWhURENxbW8wVXR0U2NGT2YzN1Z6eDE5amlEbGJuellRWUF2NnVYa1wiLFxuICAgIFwiYWxnXCI6IFwiUlMzODRcIixcbiAgICBcIm5cIjogXCJ2NHBUS0dxeng1b3JELVc4YzBkRkt5Nm15TEh0NEtlekVfeE5WenZXUFdvMUR3V0ozTXRtS1BuYnJiclB0MHBOaHVPVHVBLXp4RWR1U1o5MldsTGlNLWE5TEhVXzVMdm1jTTV6UHFjd2pwOGE1SWFyaVdieC03NE9rd1k1Nk04MEpLWlVReVZ0czNsTE5Kdi05aHpUS0J0aGVRTl92RkZOdk00ck9ueUphTE1tUENWY1Q4MXE5VUlhWHRnQWhLQ3BHdFpiZlZFbEFMR1lqeUZtYjdpTzBMWDROb1FheU1vSlhLY3FGbmY2N0dqRnB3ZzhqTVkxaGliT1J1eVJ5YVlNdUowWkpWcUFhdXp1dnVsaUxyMUx0R1BWZ292ZXdVRFV0LWtnTkZ6SGRDNmNjVF9Ed3BHbXpsR2twQjJ5ZEJ1T2NjbGxJa1NTbndYM3NvZ1NkX0dzbndcIlxufV19IiwyLDFd/fhir"
26+
client_id = "3n43riWosf7geBkZBuybgvH3MZ7XHCnsQ2w60TIl51kvCohhypVi76GKjPcYZqeKww5xghfkXON9CWZe24dcD51vgFsxQpWyKu231uazer9562RAUvTFY9Qmswad33zvXomNT9WpAfNvtNcvhOzOwdrZZ1mYCV5pHfZYYXJHvPz7mnGvrnnvHUW1VAMayZf38dM8cY3M7v5hhb2OXabgtXBTabx8Ho2rGOcaKZGcDpds8kfcaIfDJIoJD6F72MCS2PpB8UoM2tsJFANMwUCiTEoMnluu3JbPOkfy5HvcepmXaVfC5PeyF7LLTHg8ePTP1WQXODudt8biepUmdsv9eT3JmPYIw4KCnGlLvO8WFKrQqwz18KO4DjScHILUjQzDfGGjjXBUZ5v6mKoHuvEyI1ijBCPp4NvP29cEUQjcxHI22SKnkVfLXEK7w1mdRlvjAFOuMvJoERMb9Hg63OP3iGBn112r0XUhHjGuzEb9hNc63kkUXIHkDqD1PKaTxoRq1afGsta4Itr36mJQT4OwcwelKuX2F32guPzovGA5wIbG5I6iopp6a7inIeIgg84wH5DVPwRGr5Tr4GBtxphtnSr9tWzDA0noX9VhF4AehYMdG2xtbVGmkERRs0PKGHpQUYXZ7YDkxkzKwnveok4lJS73fRitWcgBZi1TaVWZPOVs2rlXVDW57k87h1rzaofgvYFpfGOdfHmP87o2VXAQ8XYnW8THoLxwcdICnV9mY0xZ8YnuKR38XvEiPlxax5ZF77YQ5sPbFy53Ru6Ci2Dly6vU9quuTVK3MJaQI1IUfRqLQFwUivtXl7iFDH0mPWQUs5skRzXKQEcnLVR2fU0TVt39MX5WnSPgoeNVwxt84hsseOKbFJzZmGXSjScbFLDqUN3DKH07KHKOs0eMIYGw0dnnlvjlQM4NTlgTyogIfgBwLfCeXuTdEHBXkImNCDDqNKsxVS9irVCWvZcDTxjrY7nt1fPW3WoGfGVjxm2q5ao71bmMHIrxHuuOwk1G1C1z"
27+
scope = "system/*.rs"
428

5-
smart_config = BackendServicesConfig(;
6-
iss = "https://whatever.smart/our-sample-backend-service",
7-
sub = client_id,
8-
private_key = JSONWebTokens.RS384(test_private_key),
9-
scope = "system/*.*",
10-
token_endpoint = token_endpoint,
11-
)
29+
# Signing key (RS384 algorithm, i.e., SHA384 hash function)
30+
key = JWTs.JWKRSA(MbedTLS.MD_SHA384, MbedTLS.parse_keyfile(joinpath(@__DIR__, "key", "private.pem")))
31+
keyid = "Yb09hTDCqmo0UttScFOf37Vzx19jiDlbnzYQYAv6uXk"
1232

13-
smart_result = backend_services(smart_config)
33+
smart_config = BackendServicesConfig(; base_url, client_id, key, keyid, scope)
1434

35+
smart_result = backend_services(smart_config)
1536
@test smart_result isa SMARTBackendServices.BackendServicesResult
1637

1738
access_token = get_fhir_access_token(smart_result)
18-
1939
@test access_token isa AbstractString
20-
2140
@test length(access_token) > 1
41+
42+
@testset "token_endpoint" begin
43+
# Correct settings
44+
token_endpoint_wellknown = SMARTBackendServices._token_endpoint_wellknown(smart_config)
45+
@test token_endpoint_wellknown isa String
46+
token_endpoint_metadata = SMARTBackendServices._token_endpoint_metadata(smart_config)
47+
@test token_endpoint_metadata isa String
48+
@test token_endpoint_metadata === token_endpoint_wellknown
49+
50+
# Incorrect base url
51+
config = BackendServicesConfig(; base_url = "https://google.com", client_id, key, keyid, scope)
52+
@test SMARTBackendServices._token_endpoint_wellknown(config) === nothing
53+
@test SMARTBackendServices._token_endpoint_metadata(config) === nothing
54+
@test_throws ErrorException("SMART configuration: Violation of the FHIR specification. The FHIR server does neither convey its SMART capabilities using a Well-Known Uniform Resource Identifiers (URIs) JSON file nor its CapabilityStatement.") backend_services(config)
55+
end

0 commit comments

Comments
 (0)