From 84ead00e4cf916c1fa064019bed48a8633af27f4 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 14 Feb 2024 23:43:13 +0100 Subject: [PATCH] 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 --- Project.toml | 12 ++- docs/Project.toml | 3 + docs/make.jl | 1 - src/SMARTBackendServices.jl | 4 +- src/backend_services.jl | 142 ++++++++++++++++++++++++++++---- src/jwt.jl | 2 +- src/timestamps.jl | 23 ------ src/types.jl | 26 +++--- test/basic.jl | 58 ++++++++++--- test/key/private.pem | 28 +++++++ test/runtests.jl | 7 +- test/test_private_key/create.jl | 36 -------- test/test_private_key/delete.jl | 1 - 13 files changed, 222 insertions(+), 121 deletions(-) delete mode 100644 src/timestamps.jl create mode 100644 test/key/private.pem delete mode 100644 test/test_private_key/create.jl delete mode 100644 test/test_private_key/delete.jl diff --git a/Project.toml b/Project.toml index 8cc52d0..e55d354 100644 --- a/Project.toml +++ b/Project.toml @@ -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"] diff --git a/docs/Project.toml b/docs/Project.toml index c6141ff..9b8a775 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,6 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" SMARTBackendServices = "78af60b6-7677-4c75-8291-bd270d1b4390" + +[compat] +Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index 6ee4f74..6542063 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,7 +15,6 @@ makedocs(; "Home" => "index.md", "API" => "api.md", ], - strict=true, ) deploydocs(; diff --git a/src/SMARTBackendServices.jl b/src/SMARTBackendServices.jl index 53ed7a8..b1a477c 100644 --- a/src/SMARTBackendServices.jl +++ b/src/SMARTBackendServices.jl @@ -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 @@ -19,6 +18,5 @@ include("types.jl") include("backend_services.jl") include("jwt.jl") -include("timestamps.jl") end # module diff --git a/src/backend_services.jl b/src/backend_services.jl index 29a641c..19146bd 100644 --- a/src/backend_services.jl +++ b/src/backend_services.jl @@ -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) diff --git a/src/jwt.jl b/src/jwt.jl index 4c97c21..1653576 100644 --- a/src/jwt.jl +++ b/src/jwt.jl @@ -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 diff --git a/src/timestamps.jl b/src/timestamps.jl deleted file mode 100644 index 7a5a3b6..0000000 --- a/src/timestamps.jl +++ /dev/null @@ -1,23 +0,0 @@ -function seconds_since_the_epoch_utc(zoned_date_time::TimeZones.ZonedDateTime) - zoned_date_time_utc = TimeZones.astimezone( - zoned_date_time, - TimeZones.tz"UTC", - ) - float_number_of_seconds_since_the_unix_epoch = TimeZones.datetime2unix( - Dates.DateTime( - zoned_date_time_utc - ) - ) - return float_number_of_seconds_since_the_unix_epoch -end - -function integer_seconds_since_the_epoch_utc(zoned_date_time::TimeZones.ZonedDateTime) - float_number_of_seconds_since_the_unix_epoch = seconds_since_the_epoch_utc( - zoned_date_time - ) - integer_number_of_seconds_since_the_unix_epoch = round( - Int, - float_number_of_seconds_since_the_unix_epoch, - ) - return integer_number_of_seconds_since_the_unix_epoch -end diff --git a/src/types.jl b/src/types.jl index fe48105..e7a1ad9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -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 diff --git a/test/basic.jl b/test/basic.jl index c97dfc3..3649bc8 100644 --- a/test/basic.jl +++ b/test/basic.jl @@ -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 diff --git a/test/key/private.pem b/test/key/private.pem new file mode 100644 index 0000000..f032d54 --- /dev/null +++ b/test/key/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/ilMoarPHmisP +5bxzR0UrLqbIse3gp7MT/E1XO9Y9ajUPBYncy2Yo+dutus+3Sk2G45O4D7PER25J +n3ZaUuIz5r0sdT/ku+ZwznM+pzCOnxrkhquJZvH7vg6TBjnozzQkplRDJW2zeUs0 +m/72HNMoG2F5A3+8UU28zis6fIlosyY8JVxPzWr1Qhpe2ACEoKka1lt9USUAsZiP +IWZvuI7Qtfg2hBrIyglcpyoWd/rsaMWnCDyMxjWGJs5G7JHJpgy4nRklWoBq7O6+ +6WIuvUu0Y9WCi97BQNS36SA0XMd0LpxxP8PCkabOUaSkHbJ0G45xyWUiRJKfBfey +iBJ38ayfAgMBAAECggEACGrLvLrzq5Ha0pgF8ArxvzQNoEQxb+3usLIls++tOoWw +TzivFkTZ+HMCdGABQMzDG2pk03HdNILvFc0sJkv+JMm/BnUgmayoM8zew6IVQC89 +v00rvJ4JhEaV3WAoq1JvuCyXB1xdtcVeyLESQz6BCQIrBmZg9qWcBPAKdkeCwlcJ +e4QQMNTXCVp5mw1SGbOJNuFcCIEdE18TlRUMXH/iu2uytuF4ta9KiG0ncd1HlAvg +JTmEdSs0u3g7jqdHpDOpPBV1yR3Z7dXW++Htut21ibS+l4nUMlY6PUqX/ekAiOJK +7keXmHh2Hu+XJmiFbhEeMCakJ8j0H0rGujBoUimUAQKBgQD6OXAfn29Yv3yuorai +brkLLruxlcuakww5XT2EkHB2W2vIsvQqbeIiA1TsrTBuwbA1LKCnT13go+btObYL +QeC/QwZ4zOUANWarLt4/CAMS8YJQceyvMbtY5y5uaa/m3efRg7wgXuo4loOsImDo +KcDLrqFCQ4xGbD9+ot8aub/SNwKBgQDD9iBJDcl17Rp9u4PBBFZKget4rAq7zrL5 +2PH56ZIekHOYh3Tz/FP5Jtw09xTuLq48eB+cd7+hFyQ7djSLMtrdIF7+Vram/hQO +yY/4xP/f7KI0VCPU4RVus9rvnRNqX/sTaQX132Hd6n81IQ46D3Ydq/jR8Xc9PkA/ +ux1d6h1k2QKBgCXhBhEzcIatzjEdnqouOsLvmyhB9eV7dzFD5SkpOG0iX8mtFXtK +0R23BWcivJ83oPYbwGIziJGWHkIxJ1bC7UPm4Jbu5YfHjbhCSxCdpOF8P+7voBXR +YHwP2x6Jz0ASvaIg4BzCYZCZcGthdOwQTghHck/2q8iJYH9KJp4EGn63AoGAecV3 +eI4Vs688OAePLyFX0gL75Ufbf6tJcpLKg0cOoumWu6DpHzicogXw7wTfP8dIRJFM +63lKXns3669fpWRbtu+HuDsAU7MtmabNTYR1kJvGjAgBICcin2EqWp3cU80DA6PO +rWQ7t6Ahnk1FPvUeq/+SpjuLokYNvMy0yghSbbkCgYAnt4z+h2JWNGQP6o+DDni2 +xce2FnfQqY8JSEgzZ6N5W3WBOMpqalKb6sSOLBSgBF+TOTPnlAjvskDTuq8uNKs6 +ztPQ3fw1Fk1hpmoJ4zCTwCHcrhx4k5wTr5zG5AjNSWHML4qRJvpAVEoHBe6sQNcE +8LXWXT5beHIGvy3g+aP5OA== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 7372a74..2c36957 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,13 +1,10 @@ using SMARTBackendServices using Test -import JSONWebTokens - -include("test_private_key/create.jl") +import JWTs +import MbedTLS @testset "SMARTBackendServices.jl" begin include("basic.jl") include("jwt.jl") end - -include("test_private_key/delete.jl") diff --git a/test/test_private_key/create.jl b/test/test_private_key/create.jl deleted file mode 100644 index 9f800f3..0000000 --- a/test/test_private_key/create.jl +++ /dev/null @@ -1,36 +0,0 @@ -test_private_key_contents = """ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAwaj5gy1tKhNCXYsw5awTpg/SrIgniRWiHQYvwYrjBMTzWDwi -wa7ZqJ/tcLCMxey8d6LkxVramchb6FGlYt6Qao2rPe4xmH1v3wzgfV8HrmS3oFr9 -xp4N6y8rkVw1oVHlv7sPNmU7sc/1i+HqSQLJkzUqcBjz5SC1lzvexgqougrxu5Mf -it6ApdauE88K/nHUNKv/93Yj3BgzL+AVSDIA/o9lxEYyYFY034hQEXiMA47g/PSX -JmRpfCYhZPo4JcFpJaHLcxYalYTxdnT/9x0giAD3kseYrm8fL+pCCqLeXiEYZ1pk -Ff6usaraTjKlbdyKgfm/ndW7rZWsmBEEE+BE0QIDAQABAoIBAETi5l5S+lHNJ/4+ -VNZdr1+SoU3d4LJSj6fjW3ls/fPkWus3+OUMXUAuIzfClVCSNTgZKWdKOhgZvReW -c04H/Tbpzc/pupqThAfDfNcbxo+yI99Wfo74CdtunADI3ArWJvNchrzCpclKP/3S -DpSJ+Unzz3Swa5FFYVK6aTPo9ru2Kjv8U4boiwwla8RrSjPGHV2RnIj246m+bFGG -jvYxZJCIVZPFjhQYOGyvrtEsBXUsNCAkxMdeMgGb3i3DRZP0Mn06gspGDQ9EL7ZU -0LSUFcNPB+1GrG0QIDAuRx8diAFwr1/T7zcpMh4bYbjyGCpvMB9V7/qDloPEXxUD -DcsGBkECgYEA69HoHOSDSEo53XZvzNd+uSz5NUe7ksentV2fSDh2fouqjID0875L -C1YetJpKTLLj4ezQ0MENOo9EOax5q05c8x8tzDqewKKk1bVAbhhZ6H1ReDUwSJR6 -hgqQ8H7dTOKlx2mhFwAq04ZA9RsjOyfb2gbWXTK8UWJx9Rtn705VyM8CgYEA0jt5 -SqcaMDlYYRxUA00/4CECzTSy32/IRtOgjdgH0oxnrkIkEYwC8UH85W1l314tyxhM -1eZzMWMnfG+Ru3/I5Ibs4XlhIddGuwCVqBTTnxk3Xl+3AjlEv8wbvfvRyhH207V5 -LgojhgyEbaxt7nDLKNqnhWDeixG9mgqa/np5QF8CgYEAg71sMlfmj4gH85SquIDa -ppBdWEqOm102b7exuCLy75cO3BdewJxaSCcAWFypjVeSvzARJ83xmflXX/Det7i7 -bIbKiwFklrFF7K2pJNgtwhfgX4s3QhTQrflRJ0l4T+9+NXzpJXROeITT3RogAHgv -9glD0CT712trxkZdNe9JNTkCgYA23sD0hBKovX7csbkXt4TRaaxChNuex50dJ0UD -YHwCLxUsfSTO0508/L0V4GyzVPBczrXXKjOskzWOG8yMhHiZdMHEw5pfa8GRM0Dc -QrtNDhbOcerhELNp38rp1hzWQcQLU5USOCE3bnLt8RAsKJJN0mvguvy6jWHQH65+ -dtbIvwKBgQCzuaM2U50rGRgzFVzkCNEf6HCgKbFei31TeXvgb4VrhJ3Tu+RmPXJH -GS2zLFh88/2D1f88F6jr2M5G601vJjAQPypDGw4RHJtsS+KjAw8TjAYr/M2f1dco -HpUHZbGip3HcM84jooRnYlyhQTMiJhXxuno3xRqLGUfinpwqqyB6YA== ------END RSA PRIVATE KEY----- -""" - -test_private_key = joinpath(mktempdir(), "test_private_key.pem") - -open(test_private_key, "w") do io - s = convert(String, strip(test_private_key_contents))::String - println(io, s) -end diff --git a/test/test_private_key/delete.jl b/test/test_private_key/delete.jl deleted file mode 100644 index 6ae9440..0000000 --- a/test/test_private_key/delete.jl +++ /dev/null @@ -1 +0,0 @@ -rm(test_private_key; force = true, recursive = true)