Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Follow the specs more closely, switch to JWTs.jl, and fix the tests #9

Merged
merged 11 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
name = "SMARTBackendServices"
uuid = "78af60b6-7677-4c75-8291-bd270d1b4390"
authors = ["Dilum Aluthge", "contributors"]
version = "1.0.1"
version = "2.0.0"

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

[compat]
HTTP = "0.9.3"
HealthBase = "1.0.1"
JSON3 = "1.5.1"
JSONWebTokens = "0.3.4, 1"
TimeZones = "1.5.3"
JWTs = "0.2.4"
URIs = "1.2"
julia = "1.5"

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

[targets]
test = ["JSONWebTokens", "Test"]
test = ["MbedTLS", "Test"]
3 changes: 3 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
SMARTBackendServices = "78af60b6-7677-4c75-8291-bd270d1b4390"

[compat]
Documenter = "1"
1 change: 0 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ makedocs(;
"Home" => "index.md",
"API" => "api.md",
],
strict=true,
)

deploydocs(;
Expand Down
4 changes: 1 addition & 3 deletions src/SMARTBackendServices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import Dates
import HTTP
import HealthBase
import JSON3
import JSONWebTokens
import JWTs
import Random
import TimeZones
import URIs

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

include("backend_services.jl")
include("jwt.jl")
include("timestamps.jl")

end # module
142 changes: 124 additions & 18 deletions src/backend_services.jl
Original file line number Diff line number Diff line change
@@ -1,45 +1,151 @@
function _backend_services_create_jwt(config::BackendServicesConfig)
function _backend_services_create_jwt(config::BackendServicesConfig, token_endpoint::AbstractString)
# Random string that uniquely identifies the JWT
jti = Random.randstring(150)

now = TimeZones.now(TimeZones.localzone())
expiration_time = now + Dates.Minute(4)
expiration_time_seconds_since_epoch_utc = integer_seconds_since_the_epoch_utc(expiration_time)

# Expiration time (integer) in seconds since "epoch"
# SHALL be no more than 5 minutes in the future
expiration_time = Dates.now(Dates.UTC) + Dates.Minute(4)
expiration_time_seconds_since_epoch_utc = round(Int, Dates.datetime2unix(expiration_time))

jwt_payload_claims_dict = Dict(
"iss" => config.iss,
"sub" => config.sub,
"aud" => config.token_endpoint,
"iss" => config.client_id,
"sub" => config.client_id,
"aud" => token_endpoint,
"jti" => jti,
"exp" => expiration_time_seconds_since_epoch_utc,
)
jwt = JSONWebTokens.encode(config.private_key, jwt_payload_claims_dict)
jwt = JWTs.JWT(; payload = jwt_payload_claims_dict)

# Sign
JWTs.sign!(jwt, config.key, config.keyid)
@assert JWTs.issigned(jwt)
@assert JWTs.kid(jwt) == config.keyid

return jwt
return string(jwt)
end

# Obtain the token endpoint from the well-known URIs
# Ref: https://www.hl7.org/fhir/smart-app-launch/backend-services.html#retrieve-well-knownsmart-configuration
function _token_endpoint_wellknown(config::BackendServicesConfig)
# Request the SMART configuration file
_config_response = HTTP.request(
"GET",
joinpath(config.base_url, ".well-known/smart-configuration");
# In principle, it should be possible to omit the header
# (and servers may ignore it anyway)
# Ref: https://www.hl7.org/fhir/smart-app-launch/conformance.html#using-well-known
headers = ("Accept" => "application/json",),
devmotion marked this conversation as resolved.
Show resolved Hide resolved
# Old servers might still only support the /metadata endpoint (even though its use for SMART capabilities is deprecated)
# Hence we do not throw an exception if the request fails but try the /metadata endpoint first
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#declaring-support-for-oauth2-endpoints
# Ref: https://www.hl7.org/fhir/smart-app-launch/conformance.html#smart-on-fhir-oauth-authorization-endpoints-and-capabilities
status_exception = false,
)

# Exit gracefully (return `nothing`) if the server does not convey its SMART capabilities using well-known URIs
if _config_response.status != 200
return nothing
end

# Extract the token endpoint from the JSON response
config_response = JSON3.read(_config_response.body)
devmotion marked this conversation as resolved.
Show resolved Hide resolved
get(config_response, :token_endpoint) do
error(

Check warning on line 53 in src/backend_services.jl

View check run for this annotation

Codecov / codecov/patch

src/backend_services.jl#L53

Added line #L53 was not covered by tests
"SMART configuration: Violation of the FHIR specification. The mandatory `token_endpoint` is missing from the Well-Known Uniform Resource Identifiers (URIs) JSON document.",
)
end::String
end

# Obtain the token endpoint from the CapabilityStatement at the /metadata endpoint
# Note: Declaring SMART capabilities using the /metadata endpoint is deprecated but old servers might still not support the well-known URIs
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#declaring-support-for-oauth2-endpoints
function _token_endpoint_metadata(config::BackendServicesConfig)
# Request the CapabilityStatement
_metadata_response = HTTP.request(
"GET",
joinpath(config.base_url, "metadata");
# We only support FHIR version R4
# Ref: https://hl7.org/fhir/R4/versioning.html#mt-version
headers = ("Accept" => "application/fhir+json; fhirVersion=4.0"),
# We throw our own, hopefully more descriptive, exception if necessary
status_exception = false,
)

# Exit gracefully (return `nothing`) if the server does not convey its SMART capabilities at the /metadata endpoint
if _metadata_response.status != 200
return nothing
end

# Extract the token endpoint from the JSON response
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#declaring-support-for-oauth2-endpoints
# Ref: https://hl7.org/fhir/R4/capabilitystatement.html
compat_statement = JSON3.read(_metadata_response.body)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use struct-mapping instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial implementation used struct mapping but it felt a bit too verbose since it requires a struct for every level of this nested structure.

rest = get(compat_statement, :rest, nothing)
if rest !== nothing
for rest in compat_statement.rest
security = get(rest, :security, nothing)
if security !== nothing
extensions = get(security, :extension, nothing)
if extensions !== nothing
for extension in extensions
if get(extension, :url, nothing) ===
"http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"
for url_value in extension.extension
if url_value.url === "token"
return url_value.valueUri::String
end
end
end
end

Check warning on line 99 in src/backend_services.jl

View check run for this annotation

Codecov / codecov/patch

src/backend_services.jl#L99

Added line #L99 was not covered by tests
end
end
end

Check warning on line 102 in src/backend_services.jl

View check run for this annotation

Codecov / codecov/patch

src/backend_services.jl#L102

Added line #L102 was not covered by tests
end

error(

Check warning on line 105 in src/backend_services.jl

View check run for this annotation

Codecov / codecov/patch

src/backend_services.jl#L105

Added line #L105 was not covered by tests
"SMART configuration: Violation of the FHIR specification. The mandatory `token` url of the OAuth2 token endpoint is missing from the FHIR CompatibilityStatement.",
)
end

# Ref: https://www.hl7.org/fhir/smart-app-launch/backend-services.html
"""
backend_services(config::BackendServicesConfig)
"""
function backend_services(config::BackendServicesConfig)
jwt = _backend_services_create_jwt(config)
# Obtain the token endpoint: Try first the well-known URI and then the /metadata endpoint (deprecated)
# On Julia >= 1.7 this can be simplified to
# token_endpoint = @something _token_endpoint_wellknown(config) _token_endpoint_metadata(config) error("...")
token_endpoint = _token_endpoint_wellknown(config)
if token_endpoint === nothing
token_endpoint = _token_endpoint_metadata(config)
if token_endpoint === nothing
# Ref: https://www.hl7.org/fhir/smart-app-launch/conformance.html#smart-on-fhir-oauth-authorization-endpoints-and-capabilities
# Ref: https://hl7.org/fhir/smart-app-launch/1.0.0/conformance/index.html#smart-on-fhir-oauth-authorization-endpoints
error(
"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.",
)
end
end

# Obtain the access token
# Ref: https://www.hl7.org/fhir/smart-app-launch/backend-services.html#obtain-access-token
# Create JWT
jwt = _backend_services_create_jwt(config, token_endpoint)

body_params = Dict{String, String}()
body_params["grant_type"] = "client_credentials"
body_params["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
body_params["client_assertion"] = jwt

if config.scope !== nothing
body_params["scope"] = config.scope
end
body_params["scope"] = config.scope

_response = HTTP.request(
"POST",
config.token_endpoint;
headers = Dict("Content-Type" => "application/x-www-form-urlencoded"),
token_endpoint;
headers = ("Content-Type" => "application/x-www-form-urlencoded",),
body = URIs.escapeuri(body_params),
)

access_token_response = JSON3.read(String(_response.body))
access_token_response = JSON3.read(_response.body)
access_token = access_token_response.access_token

access_token_is_jwt, access_token_jwt_decoded = try_decode_jwt(access_token)
Expand Down
2 changes: 1 addition & 1 deletion src/jwt.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function try_decode_jwt(contents::AbstractString)
try
jwt_decoded = JSONWebTokens.decode(JSONWebTokens.None(), contents)
jwt_decoded = JWTs.claims(JWTs.JWT(; jwt = contents))
return true, jwt_decoded
catch
end
Expand Down
23 changes: 0 additions & 23 deletions src/timestamps.jl

This file was deleted.

26 changes: 12 additions & 14 deletions src/types.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
"""
BackendServicesConfig{PK}(; kwargs...)
BackendServicesConfig{T <: JWTs.JWK}(; kwargs...)

## Required Keyword Arguments:
- `iss::String`
- `private_key::PK`
- `sub::String`
- `token_endpoint::String`

## Optional Keyword Arguments:
- `scope::Union{String, Nothing}`. Default value: `nothing`.
- `base_url`::String
- `client_id::String`
- `scope::String`
- `key::T`
- `keyid::String`
"""
Base.@kwdef struct BackendServicesConfig{PK <: JSONWebTokens.Encoding}
iss::String
private_key::PK
scope::Union{String, Nothing} = nothing
sub::String
token_endpoint::String
Base.@kwdef struct BackendServicesConfig{T <: JWTs.JWK}
base_url::String
client_id::String
scope::String
key::T
keyid::String
end

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

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

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

smart_result = backend_services(smart_config)
smart_config = BackendServicesConfig(; base_url, client_id, key, keyid, scope)

smart_result = backend_services(smart_config)
@test smart_result isa SMARTBackendServices.BackendServicesResult

access_token = get_fhir_access_token(smart_result)

@test access_token isa AbstractString

@test length(access_token) > 1

@testset "token_endpoint" begin
# Correct settings
token_endpoint_wellknown = SMARTBackendServices._token_endpoint_wellknown(smart_config)
@test token_endpoint_wellknown isa String
token_endpoint_metadata = SMARTBackendServices._token_endpoint_metadata(smart_config)
@test token_endpoint_metadata isa String
@test token_endpoint_metadata === token_endpoint_wellknown

# Incorrect base url
config = BackendServicesConfig(; base_url = "https://google.com", client_id, key, keyid, scope)
@test SMARTBackendServices._token_endpoint_wellknown(config) === nothing
@test SMARTBackendServices._token_endpoint_metadata(config) === nothing
@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)
end
Loading
Loading