Skip to content

Commit

Permalink
Follow the specs more closely, switch to JWTs.jl, and fix the tests (#9)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
devmotion authored Feb 14, 2024
1 parent 465e6ad commit 84ead00
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 121 deletions.
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",),
# 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)
get(config_response, :token_endpoint) do
error(
"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)
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
end
end
end
end

error(
"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

2 comments on commit 84ead00

@devmotion
Copy link
Member Author

Choose a reason for hiding this comment

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

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

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

Registration pull request created: JuliaRegistries/General/100953

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v2.0.0 -m "<description of version>" 84ead00e4cf916c1fa064019bed48a8633af27f4
git push origin v2.0.0

Please sign in to comment.