From d575789c9d42d0f9c0389b2c06262f5c8ac47543 Mon Sep 17 00:00:00 2001 From: bwang-icf Date: Tue, 3 Dec 2024 15:33:53 -0600 Subject: [PATCH] adding smart on fhir configuration endpoint (#1270) * adding smart on fhir configuration endpoint * fixing pylint errors * more fixing of pylint errors * removing unnecessary fields for smart configuration response * fixing url path for smart config v2 * adding authorize-post to capabilities * removing v1 response and changing all OIDC config responses to v2 * fixing smart config url pattern * fixing linter errors * updating revocation endpoint to v2 and updating swagger openid config to match between versions * removing deprecated v2 param * updating tests to handle v2 cases * removing deprecated v2 param --- apps/fhir/bluebutton/tests/test_utils.py | 2 +- apps/fhir/bluebutton/utils.py | 4 +- apps/fhir/bluebutton/views/home.py | 2 +- apps/wellknown/tests.py | 4 +- apps/wellknown/views/__init__.py | 2 +- apps/wellknown/views/openid.py | 61 ++++++++++++++++++++---- hhs_oauth_server/urls.py | 3 ++ static/openapi.yaml | 2 +- 8 files changed, 64 insertions(+), 16 deletions(-) diff --git a/apps/fhir/bluebutton/tests/test_utils.py b/apps/fhir/bluebutton/tests/test_utils.py index e21a6af56..7da820379 100644 --- a/apps/fhir/bluebutton/tests/test_utils.py +++ b/apps/fhir/bluebutton/tests/test_utils.py @@ -309,7 +309,7 @@ def test_oauth_resource_xml(self): """ request = self.factory.get('/cmsblue/fhir/v1/metadata') - result = build_oauth_resource(request, False, "xml") + result = build_oauth_resource(request, "xml") expected = "true" diff --git a/apps/fhir/bluebutton/utils.py b/apps/fhir/bluebutton/utils.py index 291bb95cf..00bc2648b 100644 --- a/apps/fhir/bluebutton/utils.py +++ b/apps/fhir/bluebutton/utils.py @@ -599,14 +599,14 @@ def get_response_text(fhir_response=None): return text_in -def build_oauth_resource(request, v2=False, format_type="json"): +def build_oauth_resource(request, format_type="json"): """ Create a resource entry for oauth endpoint(s) for insertion into the conformance/capabilityStatement :return: security """ - endpoints = build_endpoint_info(OrderedDict(), v2, issuer=base_issuer(request)) + endpoints = build_endpoint_info(OrderedDict(), issuer=base_issuer(request)) if format_type.lower() == "xml": diff --git a/apps/fhir/bluebutton/views/home.py b/apps/fhir/bluebutton/views/home.py index 02015a273..553d3ea89 100644 --- a/apps/fhir/bluebutton/views/home.py +++ b/apps/fhir/bluebutton/views/home.py @@ -66,7 +66,7 @@ def fhir_conformance(request, via_oauth=False, v2=False, *args): od = conformance_filter(text_out) # Append Security to ConformanceStatement - security_endpoint = build_oauth_resource(request, v2, format_type="json") + security_endpoint = build_oauth_resource(request, format_type="json") od['rest'][0]['security'] = security_endpoint # Fix format values od['format'] = ['application/json', 'application/fhir+json'] diff --git a/apps/wellknown/tests.py b/apps/wellknown/tests.py index 059d3ada0..9cc0fc648 100644 --- a/apps/wellknown/tests.py +++ b/apps/wellknown/tests.py @@ -21,8 +21,8 @@ def test_valid_response(self): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) self.assertContains( - response, reverse('oauth2_provider:token')) - self.assertContains(response, reverse('openid_connect_userinfo')) + response, reverse('oauth2_provider_v2:token-v2')) + self.assertContains(response, reverse('openid_connect_userinfo_v2')) self.assertContains(response, "response_types_supported") self.assertContains(response, getattr(settings, 'HOSTNAME_URL')) response_content = response.content diff --git a/apps/wellknown/views/__init__.py b/apps/wellknown/views/__init__.py index ad4db96d2..329343281 100644 --- a/apps/wellknown/views/__init__.py +++ b/apps/wellknown/views/__init__.py @@ -1,3 +1,3 @@ -from .openid import openid_configuration, base_issuer, build_endpoint_info # NOQA +from .openid import openid_configuration, smart_configuration, base_issuer, build_endpoint_info # NOQA from .application import ApplicationListView, ApplicationLabelView # NOQA from .public_applications import ApplicationListView as PublicApplicationListView # NOQA diff --git a/apps/wellknown/views/openid.py b/apps/wellknown/views/openid.py index 68f3ac9b2..2987eb316 100644 --- a/apps/wellknown/views/openid.py +++ b/apps/wellknown/views/openid.py @@ -9,6 +9,17 @@ import apps.logging.request_logger as bb2logging logger = logging.getLogger(bb2logging.HHS_SERVER_LOGNAME_FMT.format(__name__)) +SCOPES_SUPPORTED = ["profile", "patient/Patient.read", "patient/ExplanationOfBenefit.read", "patient/Coverage.read"] +CODE_CHALLENGE_METHODS_SUPPORTED = ["S256"] +CAPABILITIES = [ + "client-confidential-symmetric", + "sso-openid-connect", + "launch-standalone", + "permission-offline", + "permission-patient", + "permission-v1", + "authorize-post" +] @require_GET @@ -18,8 +29,18 @@ def openid_configuration(request): """ data = OrderedDict() issuer = base_issuer(request) - v2 = request.path.endswith('openid-configuration-v2') or request.path.endswith('openidConfigV2') - data = build_endpoint_info(data, issuer=issuer, v2=v2) + data = build_endpoint_info(data, issuer=issuer) + return JsonResponse(data) + + +@require_GET +def smart_configuration(request): + """ + Views that returns smart_configuration. + """ + data = OrderedDict() + issuer = base_issuer(request) + data = build_smart_config_endpoint(data, issuer=issuer) return JsonResponse(data) @@ -50,7 +71,7 @@ def base_issuer(request): return issuer -def build_endpoint_info(data=OrderedDict(), v2=False, issuer=""): +def build_endpoint_info(data=OrderedDict(), issuer=""): """ construct the data package issuer should be http: or https:// prefixed url. @@ -60,12 +81,12 @@ def build_endpoint_info(data=OrderedDict(), v2=False, issuer=""): """ data["issuer"] = issuer data["authorization_endpoint"] = issuer + \ - reverse('oauth2_provider:authorize' if not v2 else 'oauth2_provider_v2:authorize-v2') - data["revocation_endpoint"] = issuer + reverse('oauth2_provider:revoke') + reverse('oauth2_provider_v2:authorize-v2') + data["revocation_endpoint"] = issuer + reverse('oauth2_provider_v2:revoke-token-v2') data["token_endpoint"] = issuer + \ - reverse('oauth2_provider:token' if not v2 else 'oauth2_provider_v2:token-v2') + reverse('oauth2_provider_v2:token-v2') data["userinfo_endpoint"] = issuer + \ - reverse('openid_connect_userinfo' if not v2 else 'openid_connect_userinfo_v2') + reverse('openid_connect_userinfo_v2') data["ui_locales_supported"] = ["en-US", ] data["service_documentation"] = getattr(settings, 'DEVELOPER_DOCS_URI', @@ -82,5 +103,29 @@ def build_endpoint_info(data=OrderedDict(), v2=False, issuer=""): data["response_types_supported"] = ["code", "token"] data["fhir_metadata_uri"] = issuer + \ - reverse('fhir_conformance_metadata' if not v2 else 'fhir_conformance_metadata_v2') + reverse('fhir_conformance_metadata_v2') + return data + + +def build_smart_config_endpoint(data=OrderedDict(), issuer=""): + """ + construct the smart config endpoint response. Takes in output of build_endpoint_info since they share many fields + issuer should be http: or https:// prefixed url. + + :param data: + :return: + """ + + data = build_endpoint_info(data, issuer=issuer) + del (data["userinfo_endpoint"]) + del (data["ui_locales_supported"]) + del (data["service_documentation"]) + del (data["op_tos_uri"]) + del (data["fhir_metadata_uri"]) + data["grant_types_supported"].remove("refresh_token") + + data["scopes_supported"] = SCOPES_SUPPORTED + data["code_challenge_methods_supported"] = CODE_CHALLENGE_METHODS_SUPPORTED + data["capabilities"] = CAPABILITIES + return data diff --git a/hhs_oauth_server/urls.py b/hhs_oauth_server/urls.py index 6e9812530..5b5ae5118 100644 --- a/hhs_oauth_server/urls.py +++ b/hhs_oauth_server/urls.py @@ -6,6 +6,7 @@ from django.contrib import admin from apps.accounts.views.oauth2_profile import openidconnect_userinfo from apps.fhir.bluebutton.views.home import fhir_conformance, fhir_conformance_v2 +from apps.wellknown.views.openid import smart_configuration from hhs_oauth_server.hhs_oauth_server_context import IsAppInstalled admin.autodiscover() @@ -17,6 +18,7 @@ urlpatterns = [ path("health", include("apps.health.urls")), re_path(r"^.well-known/", include("apps.wellknown.urls")), + path("v1/fhir/.wellknown/smart-configuration", smart_configuration, name="smart_configuration"), path("forms/", include("apps.forms.urls")), path("v1/accounts/", include("apps.accounts.urls")), re_path( @@ -32,6 +34,7 @@ openidconnect_userinfo, name="openid_connect_userinfo_v2", ), + path("v2/fhir/.wellknown/smart-configuration", smart_configuration, name="smart_configuration"), path("v2/fhir/metadata", fhir_conformance_v2, name="fhir_conformance_metadata_v2"), path("v2/fhir/", include("apps.fhir.bluebutton.v2.urls")), path("v2/o/", include("apps.dot_ext.v2.urls")), diff --git a/static/openapi.yaml b/static/openapi.yaml index 646955f26..4b743978d 100755 --- a/static/openapi.yaml +++ b/static/openapi.yaml @@ -365,7 +365,7 @@ paths: description: "Error: Bad Gateway, e.g. An error occurred contacting the FHIR server." tags: - v1 - /.well-known/openid-configuration-v2: + /.well-known/openid-configuration: get: operationId: openIdConfig_v2 description: "Returns OIDC (OpenID Connect protocol) Discovery: listing of the OpenID/OAuth endpoints, supported scopes and claims (public access, no token needed)"