From a177e8bfa5c4e7358e7ab61f4c538436a28ecd96 Mon Sep 17 00:00:00 2001 From: James Demery Date: Tue, 30 Jun 2026 12:29:57 -0400 Subject: [PATCH 01/10] WIP on 4961 --- apps/dot_ext/utils.py | 12 ++ apps/dot_ext/v3/urls.py | 1 + apps/dot_ext/views/__init__.py | 10 +- apps/dot_ext/views/authorization.py | 91 +++++++++++- apps/testclient/views.py | 18 ++- requirements/requirements.dev.txt | 172 +++++++++++----------- requirements/requirements.txt | 6 +- templates/design_system/authorize_v3.html | 13 ++ 8 files changed, 226 insertions(+), 97 deletions(-) diff --git a/apps/dot_ext/utils.py b/apps/dot_ext/utils.py index 0417ae179..b20e46061 100644 --- a/apps/dot_ext/utils.py +++ b/apps/dot_ext/utils.py @@ -1,4 +1,7 @@ +import base64 +import hashlib import logging +import os import re from base64 import b64decode from http import HTTPStatus @@ -381,3 +384,12 @@ def check_auth_tracking_and_create_access_token_extension( AccessTokenExtension.objects.get_or_create( access_token=token, include_samhsa=include_samhsa, part_d_eob_only=prior_part_d_eob_only ) + + +def generate_code_verifier() -> str: + return base64.urlsafe_b64encode(os.urandom(44)).rstrip(b'=').decode('utf-8') + + +def generate_code_challenge(code_verifier: str) -> str: + code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode('ASCII')).digest()).decode('utf-8') + return code_challenge diff --git a/apps/dot_ext/v3/urls.py b/apps/dot_ext/v3/urls.py index dc43653b3..f0680611b 100644 --- a/apps/dot_ext/v3/urls.py +++ b/apps/dot_ext/v3/urls.py @@ -19,6 +19,7 @@ path('revoke_token/', waffle_switch('v3_endpoints')(views.RevokeTokenView.as_view()), name='revoke-token-v3'), path('revoke/', waffle_switch('v3_endpoints')(views.RevokeView.as_view()), name='revoke-v3'), path('introspect/', waffle_switch('v3_endpoints')(views.IntrospectTokenView.as_view()), name='introspect-v3'), + path('permission-logout', views.PermissionScreenLogoutView.as_view(), name='permission-logout'), ] diff --git a/apps/dot_ext/views/__init__.py b/apps/dot_ext/views/__init__.py index da038284b..c92a29d75 100644 --- a/apps/dot_ext/views/__init__.py +++ b/apps/dot_ext/views/__init__.py @@ -1,2 +1,10 @@ from .application import ApplicationRegistration, ApplicationUpdate, ApplicationDelete # NOQA -from .authorization import AuthorizationView, ApprovalView, TokenView, RevokeTokenView, RevokeView, IntrospectTokenView # NOQA +from .authorization import ( # NOQA + AuthorizationView, + ApprovalView, + TokenView, + RevokeTokenView, + RevokeView, + IntrospectTokenView, + PermissionScreenLogoutView, +) diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index 63e88a3cb..8d4030118 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -8,12 +8,12 @@ from functools import wraps from http import HTTPStatus from time import strftime -from urllib.parse import parse_qs, urlparse +from urllib.parse import parse_qs, urlencode, urlparse import jwt import waffle from django.conf import settings -from django.contrib.auth import get_user_model +from django.contrib.auth import get_user_model, logout from django.contrib.auth.models import User from django.contrib.auth.views import redirect_to_login from django.core.cache import cache @@ -21,9 +21,11 @@ from django.db.models import Q from django.http import HttpRequest, JsonResponse from django.http.response import HttpResponse, HttpResponseBadRequest +from django.shortcuts import redirect from django.template.response import TemplateResponse from django.urls import reverse from django.utils.decorators import method_decorator +from django.views import View from django.views.decorators.csrf import csrf_exempt from django.views.decorators.debug import sensitive_post_parameters from fhir.resources.R4B.address import Address @@ -65,6 +67,7 @@ APPLICATION_DOES_NOT_HAVE_V3_ENABLED_YET, CLIENT_CREDENTIALS, CLIENT_CREDENTIALS_ACCEPTED_JWT_ALGORITHMS, + CODE_CHALLENGE_METHOD_S256, HHS_SERVER_LOGNAME_FMT, OPENID_SCOPE, USER_TYPE_ALIGNED_NETWORKS_BENEFICIARY, @@ -101,6 +104,8 @@ from apps.dot_ext.signals import beneficiary_authorized_application from apps.dot_ext.utils import ( check_auth_tracking_and_create_access_token_extension, + generate_code_challenge, + generate_code_verifier, get_api_version_number_from_url, json_response_from_oauth2_error, remove_application_user_pair_tokens_data_access, @@ -258,7 +263,7 @@ def get_context_data(self, **kwargs): context = super(AuthorizationView, self).get_context_data(**kwargs) context['permission_end_date_text'] = self.application.access_end_date_text() context['permission_end_date'] = self.application.access_end_date() - + print('self.application in get_context_data: ', self.application.__dict__) if 'form' in context and self.version == Versions.V3: # By setting this to matching_scopes instead of application_scopes, we ensure that the scopes # for the access token are in the intersection of what the application is allowed to have and @@ -326,6 +331,7 @@ def dispatch(self, request, *args, **kwargs): return param_check request.session['version'] = self.version + print('WE ARE IN DISPATCH: ', request.session.__dict__) # Accept lang from GET or POST lang = self._get_param(request, 'lang') @@ -334,10 +340,13 @@ def dispatch(self, request, *args, **kwargs): if request.method == 'POST' and not request.user.is_authenticated: post_qs = request.POST.urlencode() + print('POST_QS: ', post_qs) # preserve existing query too existing_qs = request.META.get('QUERY_STRING', '') + print('existing_qs: ', existing_qs) merged_qs = f'{existing_qs}&{post_qs}' if existing_qs else post_qs next_url = f'{request.path}?{merged_qs}' + print('next_url: ', next_url) return redirect_to_login(next_url, login_url=self.login_url) return super().dispatch(request, *args, **kwargs) @@ -1345,3 +1354,79 @@ def post(self, request, *args, **kwargs): return json_response_from_oauth2_error(error) return super(IntrospectTokenView, self).post(request, args, kwargs) + + +class PermissionScreenLogoutView(View): + # def post(self, request, *args, **kwargs): + # print('WE ARE IN THE POST22: ', request.__dict__) + # print('WE ARE IN THE POST22 SESSION: ', request.session.__dict__) + # request.session.pop('token', None) + # logout(request) + # request.session['version'] = 3 + # request.session['api_ver'] = 3 + # # Missing a bunch of different env vars needed to get through + # # return redirect( + # # 'https://test.medicare.gov/account/login/?client_id=bb2api&redirect_uri=http://localhost:8000/mymedicare/sls-callback&relay=88239279635745734637300171&lang=en-us' + # # ) + # # state is not there with this URL + # # return redirect('https://test.medicare.gov/sso/authorize?client_id=bb2api') + # # Not a real URL + # # return redirect('https://test.medicare.gov/sso/login?client_id=bb2api') + # # Invalid + # # return redirect('https://test.medicare.gov/sso/session?client_id=bb2api') + # # Not a real URL either + # # return redirect('https://test.medicare.gov/sso/session/authorize?client_id=bb2api') + # # invalid_client - app id failed + # return redirect('http://localhost:8000/mymedicare/login') + + def post(self, request, *args, **kwargs): + # Save version before logout clears the session + version = request.session.get('version', None) + print('REQUEST POST CHECK: ', request.POST) + print('REQUEST GET CHECK: ', request.GET) + print('REQUEST SESSION CHECK: ', request.session.__dict__) + print('ARGS CHECK: ', args) + print('KWARGS CHECK: ', kwargs) + + code_verifier = generate_code_verifier() + code_challenge = generate_code_challenge(code_verifier) + redirect_uri = 'http://localhost:8000/mymedicare/sls-callback' + state = 'IQKLuHp4pqeaGDjd' + + # Save the original OAuth params before logout + oauth_params = { + 'client_id': request.GET.get('client_id') + or request.session.get('client_id') + or request.session.get('auth_client_id'), + 'redirect_uri': request.GET.get('redirect_uri') or request.session.get('redirect_uri') or redirect_uri, + 'response_type': request.GET.get('response_type') or request.session.get('response_type'), + # 'scope': request.GET.get('scope') or request.session.get('scope'), + 'state': request.GET.get('state') or request.session.get('state') or state, + 'code_challenge_method': CODE_CHALLENGE_METHOD_S256, + 'code_challenge': code_challenge, + 'scope': 'patient/Patient.rs patient/Coverage.rs patient/ExplanationOfBenefit.rs profile', + } + print('oauth params: ', oauth_params) + + # Remove None values + oauth_params = {k: v for k, v in oauth_params.items() if v is not None} + + # Clear token and log out + request.session.pop('token', None) + logout(request) + + # Restore version after logout + if version is not None: + request.session['version'] = version + + # Rebuild the authorize URL with original params + base_url = request.build_absolute_uri('/').rstrip('/') + authorize_url = f'{base_url}/v3/o/authorize?{urlencode(oauth_params)}' + + return redirect(authorize_url) + + # def post(self, request, *args, **kwargs): + # # SLSx client instance + # slsx_client = OAuth2ConfigSLSx() + # slsx_client.user_signout(request) + # return redirect('http://localhost:8000/v3/o/authorize/') diff --git a/apps/testclient/views.py b/apps/testclient/views.py index 281ac932a..2d5c58c23 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -101,8 +101,16 @@ def _convert_response_string_to_json(json_response: str) -> Dict[str, object]: def _get_oauth2_session_with_redirect(request: HttpRequest) -> OAuth2Session: - client_id = request.session['client_id'] - redirect_uri = request.session['redirect_uri'] + print('_get_oauth2_session_with_redirect session check: ', request.session.__dict__) + print('_get_oauth2_session_with_redirect request check: ', request.__dict__) + if request.session.get('client_id'): + client_id = request.session['client_id'] + else: + client_id = request.session['auth_client_id'] + if request.session.get('redirect_uri'): + redirect_uri = request.session['redirect_uri'] + else: + redirect_uri = 'http://localhost:8000/mymedicare/sls-callback' return OAuth2Session(client_id, redirect_uri=redirect_uri) @@ -177,6 +185,8 @@ def callback(request: HttpRequest): # However, the default is `v0`, which is an invalid version. This keeps it of the # same type as valid values, but does not allow us to proceed if something has broken. version = request.session.get('api_ver', Versions.NOT_AN_API_VERSION) + print('what is the request.session: ', request.session.__dict__) + print('what is the version: ', version) match version: case Versions.V1: token_uri += reverse('oauth2_provider:token') @@ -187,7 +197,7 @@ def callback(request: HttpRequest): case _: logger.error(f'Failed to get valid API version back from authorizing agent. Given: [{version}]') return ResponseErrors.MissingCallbackVersionContext(version) - + print('IN CALLBACK ABOUT TO CALL _get_oauth2_session_with_redirect') oas = _get_oauth2_session_with_redirect(request) try: # Default the CV to '' if it is not part of the session. @@ -314,7 +324,7 @@ def _link_session_or_version_is_bad(session, version): def _authorize_link(request: HttpRequest, version=Versions.NOT_AN_API_VERSION): request.session.update(setup_testclient_http_response(version=version)) - + print('IN AUTHORIZE LINK ABOUT TO CALL _get_oauth2_session_with_redirect') oas = _get_oauth2_session_with_redirect(request) # We need scopes in V3. diff --git a/requirements/requirements.dev.txt b/requirements/requirements.dev.txt index 06b1f37ae..f6b698361 100644 --- a/requirements/requirements.dev.txt +++ b/requirements/requirements.dev.txt @@ -512,9 +512,9 @@ djangorestframework-csv==3.0.0 \ --hash=sha256:46b39864f68cf42e7357a8be6109a85d484be52d6d8720bfac5e62163ec1a300 \ --hash=sha256:d6b2ee9a5fc62f98f6f4af241928836e2a2efab384d33def8ec722f9301602bf # via -r /code/requirements/requirements.in -fhir-core==1.1.8 \ - --hash=sha256:1a576ce3f6ec2aa3e86891dd00091ee03bcdcbb5a2f179f4f8e09b25b4d75047 \ - --hash=sha256:61b1fd984c1f3fc4ad2c4d96824d9ec6f92d19e9aa49269c89b9ebec878e778e +fhir-core==1.1.9 \ + --hash=sha256:61f23a711199d8d74390fd473cc5ddf75a1f4def97fd8e9f59a28bc9d5222261 \ + --hash=sha256:e3ee5912c1203993e915730e3eb23e40234f154822a45f15fadf51c76a17e452 # via fhir-resources fhir-resources==8.2.0 \ --hash=sha256:122a8b336385d504b399df9c5f332a7d913f07bc71a52dc9dde39abb5bf29129 \ @@ -672,86 +672,86 @@ geventhttpclient==2.3.9 \ --hash=sha256:f7cf60062d3aebd5e83f4d197a59609194effe25a25bcab01ae3775be18c877e \ --hash=sha256:f9d32fc9ba6d82c4f8586a6bd6e99c7e15a25404f94743dce00367aec826809d # via locust -greenlet==3.5.2 \ - --hash=sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421 \ - --hash=sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574 \ - --hash=sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad \ - --hash=sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34 \ - --hash=sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473 \ - --hash=sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32 \ - --hash=sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094 \ - --hash=sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4 \ - --hash=sha256:1724499fc08388208408681c53c5062e9803c334e5a0bdaeb616228ba882aac8 \ - --hash=sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea \ - --hash=sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb \ - --hash=sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8 \ - --hash=sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3 \ - --hash=sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682 \ - --hash=sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163 \ - --hash=sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c \ - --hash=sha256:2c6d6bfa4fdd7c39a0dbf112cdf28edbd19c517c810eefb6e4e71b0d55933a4c \ - --hash=sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f \ - --hash=sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c \ - --hash=sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742 \ - --hash=sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32 \ - --hash=sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de \ - --hash=sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904 \ - --hash=sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e \ - --hash=sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a \ - --hash=sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5 \ - --hash=sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f \ - --hash=sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef \ - --hash=sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16 \ - --hash=sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8 \ - --hash=sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b \ - --hash=sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2 \ - --hash=sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9 \ - --hash=sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3 \ - --hash=sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d \ - --hash=sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f \ - --hash=sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39 \ - --hash=sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6 \ - --hash=sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f \ - --hash=sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9 \ - --hash=sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce \ - --hash=sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00 \ - --hash=sha256:87359c23eb4e8f1b16da68faad29bf5aeb80e3628d7d8e4aa2e41c36879ddedd \ - --hash=sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146 \ - --hash=sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3 \ - --hash=sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c \ - --hash=sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e \ - --hash=sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8 \ - --hash=sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f \ - --hash=sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb \ - --hash=sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5 \ - --hash=sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3 \ - --hash=sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317 \ - --hash=sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a \ - --hash=sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d \ - --hash=sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad \ - --hash=sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014 \ - --hash=sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0 \ - --hash=sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1 \ - --hash=sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e \ - --hash=sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5 \ - --hash=sha256:bc18b8d33e6976804b9b792fe11cb3b1fee8b646e8a9e20bf521a429ddf73520 \ - --hash=sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7 \ - --hash=sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a \ - --hash=sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c \ - --hash=sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1 \ - --hash=sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73 \ - --hash=sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9 \ - --hash=sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5 \ - --hash=sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f \ - --hash=sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95 \ - --hash=sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e \ - --hash=sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c \ - --hash=sha256:e976f9f6941f57d87a194c91868622c8b22a142a741d2fde31655c319133ade6 \ - --hash=sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7 \ - --hash=sha256:f4d67c1684db3f9782c37ee4bade3f86f5a23a8fcf3f8359224106018ca40728 \ - --hash=sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4 \ - --hash=sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74 \ - --hash=sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c +greenlet==3.5.3 \ + --hash=sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db \ + --hash=sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7 \ + --hash=sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8 \ + --hash=sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc \ + --hash=sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da \ + --hash=sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8 \ + --hash=sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91 \ + --hash=sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c \ + --hash=sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310 \ + --hash=sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3 \ + --hash=sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce \ + --hash=sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149 \ + --hash=sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d \ + --hash=sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34 \ + --hash=sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be \ + --hash=sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2 \ + --hash=sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b \ + --hash=sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0 \ + --hash=sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71 \ + --hash=sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d \ + --hash=sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04 \ + --hash=sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227 \ + --hash=sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c \ + --hash=sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6 \ + --hash=sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8 \ + --hash=sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21 \ + --hash=sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23 \ + --hash=sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605 \ + --hash=sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128 \ + --hash=sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f \ + --hash=sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea \ + --hash=sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81 \ + --hash=sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2 \ + --hash=sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8 \ + --hash=sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e \ + --hash=sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb \ + --hash=sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47 \ + --hash=sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8 \ + --hash=sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab \ + --hash=sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7 \ + --hash=sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861 \ + --hash=sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702 \ + --hash=sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814 \ + --hash=sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf \ + --hash=sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a \ + --hash=sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260 \ + --hash=sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1 \ + --hash=sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec \ + --hash=sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a \ + --hash=sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5 \ + --hash=sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1 \ + --hash=sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4 \ + --hash=sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c \ + --hash=sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda \ + --hash=sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb \ + --hash=sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31 \ + --hash=sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d \ + --hash=sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c \ + --hash=sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d \ + --hash=sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b \ + --hash=sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550 \ + --hash=sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c \ + --hash=sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357 \ + --hash=sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c \ + --hash=sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4 \ + --hash=sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930 \ + --hash=sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8 \ + --hash=sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b \ + --hash=sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb \ + --hash=sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16 \ + --hash=sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608 \ + --hash=sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f \ + --hash=sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc \ + --hash=sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d \ + --hash=sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44 \ + --hash=sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b \ + --hash=sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3 \ + --hash=sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154 \ + --hash=sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117 # via gevent gunicorn==25.0.3 \ --hash=sha256:aca364c096c81ca11acd4cede0aaeea91ba76ca74e2c0d7f879154db9d890f35 \ @@ -1502,9 +1502,9 @@ python-dotenv==1.2.2 \ --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via -r /code/requirements/requirements.in -python-engineio==4.13.2 \ - --hash=sha256:8c101cd170e400dc4e970cd523325cde22df8fc25140953f379327055d701a6b \ - --hash=sha256:a7732e99cfb7db6ed1aee31f18d7f73bbae086a92f31dee019bc646155d9684e +python-engineio==4.13.3 \ + --hash=sha256:1f60ecaf1358190f0e26c48c578a60428dc02a8f1295bc3dbf53d1b31116821f \ + --hash=sha256:572b7783e341fed21edbc7cea297ccd378dad79265fdde96aa4664420a7c06c9 # via # locust # python-socketio diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1a3b2fa0c..d3d8e7379 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -347,9 +347,9 @@ djangorestframework-csv==3.0.0 \ --hash=sha256:46b39864f68cf42e7357a8be6109a85d484be52d6d8720bfac5e62163ec1a300 \ --hash=sha256:d6b2ee9a5fc62f98f6f4af241928836e2a2efab384d33def8ec722f9301602bf # via -r /code/requirements/requirements.in -fhir-core==1.1.8 \ - --hash=sha256:1a576ce3f6ec2aa3e86891dd00091ee03bcdcbb5a2f179f4f8e09b25b4d75047 \ - --hash=sha256:61b1fd984c1f3fc4ad2c4d96824d9ec6f92d19e9aa49269c89b9ebec878e778e +fhir-core==1.1.9 \ + --hash=sha256:61f23a711199d8d74390fd473cc5ddf75a1f4def97fd8e9f59a28bc9d5222261 \ + --hash=sha256:e3ee5912c1203993e915730e3eb23e40234f154822a45f15fadf51c76a17e452 # via fhir-resources fhir-resources==8.2.0 \ --hash=sha256:122a8b336385d504b399df9c5f332a7d913f07bc71a52dc9dde39abb5bf29129 \ diff --git a/templates/design_system/authorize_v3.html b/templates/design_system/authorize_v3.html index 398a5966a..854cfe317 100644 --- a/templates/design_system/authorize_v3.html +++ b/templates/design_system/authorize_v3.html @@ -151,6 +151,19 @@

+ {% trans "Not you? " %} +
+ {% csrf_token %} + +
+ + {% if not error %} From 56d508a3fc6185240f4a962b18d76a9015e76d9d Mon Sep 17 00:00:00 2001 From: James Demery Date: Wed, 1 Jul 2026 13:02:58 -0400 Subject: [PATCH 02/10] More work - postman working but not test client yet, seemingly due to missing code_verifier --- apps/dot_ext/views/authorization.py | 86 ++++++++++++----------------- apps/testclient/views.py | 5 ++ 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index 8d4030118..82c9a15bb 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -104,8 +104,6 @@ from apps.dot_ext.signals import beneficiary_authorized_application from apps.dot_ext.utils import ( check_auth_tracking_and_create_access_token_extension, - generate_code_challenge, - generate_code_verifier, get_api_version_number_from_url, json_response_from_oauth2_error, remove_application_user_pair_tokens_data_access, @@ -263,7 +261,17 @@ def get_context_data(self, **kwargs): context = super(AuthorizationView, self).get_context_data(**kwargs) context['permission_end_date_text'] = self.application.access_end_date_text() context['permission_end_date'] = self.application.access_end_date() - print('self.application in get_context_data: ', self.application.__dict__) + print('self.request in get_context_data: ', self.request.__dict__) + print('self.request.GET.code in get_context_data: ', self.request.GET.get('code')) + self.request.session['oauth_params'] = { + 'client_id': self.request.GET.get('client_id'), + 'redirect_uri': self.request.GET.get('redirect_uri'), + 'response_type': self.request.GET.get('response_type'), + 'scope': self.request.GET.get('scope'), + 'state': self.request.GET.get('state'), + 'code_challenge': self.request.GET.get('code_challenge'), + 'code_challenge_method': self.request.GET.get('code_challenge_method'), + } if 'form' in context and self.version == Versions.V3: # By setting this to matching_scopes instead of application_scopes, we ensure that the scopes # for the access token are in the intersection of what the application is allowed to have and @@ -340,13 +348,10 @@ def dispatch(self, request, *args, **kwargs): if request.method == 'POST' and not request.user.is_authenticated: post_qs = request.POST.urlencode() - print('POST_QS: ', post_qs) # preserve existing query too existing_qs = request.META.get('QUERY_STRING', '') - print('existing_qs: ', existing_qs) merged_qs = f'{existing_qs}&{post_qs}' if existing_qs else post_qs next_url = f'{request.path}?{merged_qs}' - print('next_url: ', next_url) return redirect_to_login(next_url, login_url=self.login_url) return super().dispatch(request, *args, **kwargs) @@ -404,6 +409,7 @@ def validate_v3_authorization_request(self): raise AccessDeniedError(description='Unable to verify permission.') def form_valid(self, form): + print('WE ARE IN FORM_VALID') client_id = form.cleaned_data['client_id'] application = get_application_model().objects.get(client_id=client_id) @@ -502,7 +508,7 @@ def form_valid(self, form): # Extract code from url url_query = parse_qs(urlparse(self.success_url).query) code = url_query.get('code', [None])[0] - + print('code in form_valid: ', code) # Default the user sharing their SAMHSA data to True, and if it is v3, check to see what the value is user_approves_sharing_samhsa_data = True if self.version == Versions.V3: @@ -1090,6 +1096,7 @@ def _retrieve_prior_include_samhsa_and_part_d_eob_only_values( def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: version = get_api_version_number_from_url(self.request.path_info) grant_type = request.POST.get('grant_type') + print('we are in post of token view') try: # If it is not version 3, we don't need to check that the application is in the v3_early_adopter flag, # just continue with standard validation. @@ -1098,7 +1105,7 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: self._validate_v3_token_call(request) app = validate_app_is_active(request) - + print('app post: ', app) if grant_type == 'authorization_code' and app.allowed_auth_type == 'CLIENT_CREDENTIALS': error_message = APPLICATION_HAS_CLIENT_CREDENTIALS_ENABLED_NON_CLIENT_CREDENTIALS_AUTH_CALL_MADE.format( app.name @@ -1203,9 +1210,9 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: prior_include_samhsa, prior_part_d_eob_only = self._retrieve_prior_include_samhsa_and_part_d_eob_only_values( grant_type, request, app.part_d_eob_only ) - + print('calling token response POST: ', request.POST) url, headers, body, status = self.create_token_response(request) - + print('after token response: ', url, body, status) # retrieve the access token, update user_id with the user.id sourced above if status == HTTPStatus.OK: body = json.loads(body) @@ -1357,54 +1364,31 @@ def post(self, request, *args, **kwargs): class PermissionScreenLogoutView(View): - # def post(self, request, *args, **kwargs): - # print('WE ARE IN THE POST22: ', request.__dict__) - # print('WE ARE IN THE POST22 SESSION: ', request.session.__dict__) - # request.session.pop('token', None) - # logout(request) - # request.session['version'] = 3 - # request.session['api_ver'] = 3 - # # Missing a bunch of different env vars needed to get through - # # return redirect( - # # 'https://test.medicare.gov/account/login/?client_id=bb2api&redirect_uri=http://localhost:8000/mymedicare/sls-callback&relay=88239279635745734637300171&lang=en-us' - # # ) - # # state is not there with this URL - # # return redirect('https://test.medicare.gov/sso/authorize?client_id=bb2api') - # # Not a real URL - # # return redirect('https://test.medicare.gov/sso/login?client_id=bb2api') - # # Invalid - # # return redirect('https://test.medicare.gov/sso/session?client_id=bb2api') - # # Not a real URL either - # # return redirect('https://test.medicare.gov/sso/session/authorize?client_id=bb2api') - # # invalid_client - app id failed - # return redirect('http://localhost:8000/mymedicare/login') - def post(self, request, *args, **kwargs): # Save version before logout clears the session version = request.session.get('version', None) - print('REQUEST POST CHECK: ', request.POST) + print('REQUEST CHECK: ', request.__dict__) print('REQUEST GET CHECK: ', request.GET) print('REQUEST SESSION CHECK: ', request.session.__dict__) - print('ARGS CHECK: ', args) - print('KWARGS CHECK: ', kwargs) - - code_verifier = generate_code_verifier() - code_challenge = generate_code_challenge(code_verifier) - redirect_uri = 'http://localhost:8000/mymedicare/sls-callback' - state = 'IQKLuHp4pqeaGDjd' # Save the original OAuth params before logout oauth_params = { 'client_id': request.GET.get('client_id') or request.session.get('client_id') or request.session.get('auth_client_id'), - 'redirect_uri': request.GET.get('redirect_uri') or request.session.get('redirect_uri') or redirect_uri, - 'response_type': request.GET.get('response_type') or request.session.get('response_type'), - # 'scope': request.GET.get('scope') or request.session.get('scope'), - 'state': request.GET.get('state') or request.session.get('state') or state, + 'redirect_uri': request.GET.get('redirect_uri') + or request.session.get('redirect_uri') + or request.session['oauth_params'].get('redirect_uri'), + 'response_type': request.GET.get('response_type') + or request.session.get('response_type') + or request.session['oauth_params'].get('response_type'), + 'state': request.GET.get('state') + or request.session.get('state') + or request.session['oauth_params'].get('state'), 'code_challenge_method': CODE_CHALLENGE_METHOD_S256, - 'code_challenge': code_challenge, - 'scope': 'patient/Patient.rs patient/Coverage.rs patient/ExplanationOfBenefit.rs profile', + 'code_challenge': request.session['oauth_params'].get('code_challenge'), + 'scope': request.session['oauth_params'].get('scope'), + 'code_verifier': request.session.get('code_verifier'), } print('oauth params: ', oauth_params) @@ -1415,18 +1399,16 @@ def post(self, request, *args, **kwargs): request.session.pop('token', None) logout(request) + # this is to avoid a corrupted session warning + request.session.cycle_key() + # Restore version after logout if version is not None: request.session['version'] = version + request.session['api_ver'] = version # Rebuild the authorize URL with original params base_url = request.build_absolute_uri('/').rstrip('/') authorize_url = f'{base_url}/v3/o/authorize?{urlencode(oauth_params)}' return redirect(authorize_url) - - # def post(self, request, *args, **kwargs): - # # SLSx client instance - # slsx_client = OAuth2ConfigSLSx() - # slsx_client.user_signout(request) - # return redirect('http://localhost:8000/v3/o/authorize/') diff --git a/apps/testclient/views.py b/apps/testclient/views.py index 2d5c58c23..c6ac070db 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -205,7 +205,12 @@ def callback(request: HttpRequest): # It is not clear, now (2025) why it should be '' instead of `None`. # Perhaps oas.fetch_token fails (and raises a `MissingTokenError`) if the code verifier # cannot be pulled from the session. + print('REQUEST SESSION: ', request.session.__dict__) cv = request.session.get('code_verifier', '') + if not cv: + print('if eval') + cv = request.session.get('oauth_params', {}).get('code_verifier') + print('what is the cv: ', cv) token = oas.fetch_token( token_uri, client_secret=get_client_secret(), authorization_response=auth_uri, code_verifier=cv ) From 0b95685a06b6c475f3c2c9c589e2a4ea7bcea7a3 Mon Sep 17 00:00:00 2001 From: James Demery Date: Wed, 1 Jul 2026 15:56:31 -0400 Subject: [PATCH 03/10] Continuing to try and make testclient work, postman auth flow works with switch account --- apps/dot_ext/views/authorization.py | 22 ++++++++-------------- apps/testclient/views.py | 18 ++++++++---------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index d774f0ee8..d108bff32 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -262,8 +262,7 @@ def get_context_data(self, **kwargs): context = super(AuthorizationView, self).get_context_data(**kwargs) context['permission_end_date_text'] = self.application.access_end_date_text() context['permission_end_date'] = self.application.access_end_date() - print('self.request in get_context_data: ', self.request.__dict__) - print('self.request.GET.code in get_context_data: ', self.request.GET.get('code')) + self.request.session['oauth_params'] = { 'client_id': self.request.GET.get('client_id'), 'redirect_uri': self.request.GET.get('redirect_uri'), @@ -340,7 +339,6 @@ def dispatch(self, request, *args, **kwargs): return param_check request.session['version'] = self.version - print('WE ARE IN DISPATCH: ', request.session.__dict__) # Accept lang from GET or POST lang = self._get_param(request, 'lang') @@ -410,7 +408,6 @@ def validate_v3_authorization_request(self): raise AccessDeniedError(description='Unable to verify permission.') def form_valid(self, form): - print('WE ARE IN FORM_VALID') client_id = form.cleaned_data['client_id'] application = get_application_model().objects.get(client_id=client_id) @@ -509,7 +506,7 @@ def form_valid(self, form): # Extract code from url url_query = parse_qs(urlparse(self.success_url).query) code = url_query.get('code', [None])[0] - print('code in form_valid: ', code) + # Default the user sharing their SAMHSA data to True, and if it is v3, check to see what the value is user_approves_sharing_samhsa_data = True if self.version == Versions.V3: @@ -1129,7 +1126,7 @@ def _retrieve_prior_include_samhsa_and_part_d_eob_only_values( def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: version = get_api_version_number_from_url(self.request.path_info) grant_type = request.POST.get('grant_type') - print('we are in post of token view') + try: # If it is not version 3, we don't need to check that the application is in the v3_early_adopter flag, # just continue with standard validation. @@ -1138,7 +1135,7 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: self._validate_v3_token_call(request) app = validate_app_is_active(request) - print('app post: ', app) + if grant_type == 'authorization_code' and app.allowed_auth_type == 'CLIENT_CREDENTIALS': error_message = APPLICATION_HAS_CLIENT_CREDENTIALS_ENABLED_NON_CLIENT_CREDENTIALS_AUTH_CALL_MADE.format( app.name @@ -1243,9 +1240,9 @@ def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: prior_include_samhsa, prior_part_d_eob_only = self._retrieve_prior_include_samhsa_and_part_d_eob_only_values( grant_type, request, app.part_d_eob_only ) - print('calling token response POST: ', request.POST) + url, headers, body, status = self.create_token_response(request) - print('after token response: ', url, body, status) + # retrieve the access token, update user_id with the user.id sourced above if status == HTTPStatus.OK: body = json.loads(body) @@ -1400,9 +1397,6 @@ class PermissionScreenLogoutView(View): def post(self, request, *args, **kwargs): # Save version before logout clears the session version = request.session.get('version', None) - print('REQUEST CHECK: ', request.__dict__) - print('REQUEST GET CHECK: ', request.GET) - print('REQUEST SESSION CHECK: ', request.session.__dict__) # Save the original OAuth params before logout oauth_params = { @@ -1423,7 +1417,6 @@ def post(self, request, *args, **kwargs): 'scope': request.session['oauth_params'].get('scope'), 'code_verifier': request.session.get('code_verifier'), } - print('oauth params: ', oauth_params) # Remove None values oauth_params = {k: v for k, v in oauth_params.items() if v is not None} @@ -1432,12 +1425,13 @@ def post(self, request, *args, **kwargs): request.session.pop('token', None) logout(request) - # this is to avoid a corrupted session warning + # this is to avoid a corrupted session warning - though I am still seeing that warning request.session.cycle_key() # Restore version after logout if version is not None: request.session['version'] = version + # this is the key that the testclient looks for request.session['api_ver'] = version # Rebuild the authorize URL with original params diff --git a/apps/testclient/views.py b/apps/testclient/views.py index c6ac070db..4ed879361 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -101,8 +101,6 @@ def _convert_response_string_to_json(json_response: str) -> Dict[str, object]: def _get_oauth2_session_with_redirect(request: HttpRequest) -> OAuth2Session: - print('_get_oauth2_session_with_redirect session check: ', request.session.__dict__) - print('_get_oauth2_session_with_redirect request check: ', request.__dict__) if request.session.get('client_id'): client_id = request.session['client_id'] else: @@ -185,8 +183,7 @@ def callback(request: HttpRequest): # However, the default is `v0`, which is an invalid version. This keeps it of the # same type as valid values, but does not allow us to proceed if something has broken. version = request.session.get('api_ver', Versions.NOT_AN_API_VERSION) - print('what is the request.session: ', request.session.__dict__) - print('what is the version: ', version) + match version: case Versions.V1: token_uri += reverse('oauth2_provider:token') @@ -197,7 +194,7 @@ def callback(request: HttpRequest): case _: logger.error(f'Failed to get valid API version back from authorizing agent. Given: [{version}]') return ResponseErrors.MissingCallbackVersionContext(version) - print('IN CALLBACK ABOUT TO CALL _get_oauth2_session_with_redirect') + oas = _get_oauth2_session_with_redirect(request) try: # Default the CV to '' if it is not part of the session. @@ -205,12 +202,14 @@ def callback(request: HttpRequest): # It is not clear, now (2025) why it should be '' instead of `None`. # Perhaps oas.fetch_token fails (and raises a `MissingTokenError`) if the code verifier # cannot be pulled from the session. - print('REQUEST SESSION: ', request.session.__dict__) + cv = request.session.get('code_verifier', '') + + # This conditional was an attempt to retrieve the code_verifier, but it is no + # longer available on the request at this point, not sure why if not cv: - print('if eval') - cv = request.session.get('oauth_params', {}).get('code_verifier') - print('what is the cv: ', cv) + cv = request.session.get('oauth_params', {}).get('code_verifier', '') + token = oas.fetch_token( token_uri, client_secret=get_client_secret(), authorization_response=auth_uri, code_verifier=cv ) @@ -329,7 +328,6 @@ def _link_session_or_version_is_bad(session, version): def _authorize_link(request: HttpRequest, version=Versions.NOT_AN_API_VERSION): request.session.update(setup_testclient_http_response(version=version)) - print('IN AUTHORIZE LINK ABOUT TO CALL _get_oauth2_session_with_redirect') oas = _get_oauth2_session_with_redirect(request) # We need scopes in V3. From 37d6f305f3887a06e4b52ce550f538c4a8bc6393 Mon Sep 17 00:00:00 2001 From: James Demery Date: Wed, 8 Jul 2026 15:42:26 -0400 Subject: [PATCH 04/10] Make switch account link work for testclient. Fix/add some tests --- apps/dot_ext/tests/test_authorization.py | 3 ++ apps/dot_ext/views/authorization.py | 8 ++- apps/testclient/tests.py | 47 ++++++++++++++++ apps/testclient/utils.py | 19 ++++--- apps/testclient/views.py | 22 ++++---- templates/design_system/authorize_v3.html | 65 ++++++++++++++++------- 6 files changed, 128 insertions(+), 36 deletions(-) diff --git a/apps/dot_ext/tests/test_authorization.py b/apps/dot_ext/tests/test_authorization.py index 094a239e8..42328a8bd 100644 --- a/apps/dot_ext/tests/test_authorization.py +++ b/apps/dot_ext/tests/test_authorization.py @@ -1669,6 +1669,7 @@ def test_v3_sets_scopes_in_kwargs(self, mock_pc): view = AuthorizationView(version=Versions.V3) mock_application = MagicMock() request = HttpRequest() + request.session = {} request.beneficiary_name = 'Test A User' view.request = request view.application = mock_application @@ -1703,6 +1704,7 @@ def test_v2_does_not_scopes_in_kwargs(self, mock_pc): view = AuthorizationView(version=Versions.V2) mock_application = MagicMock() request = HttpRequest() + request.session = {} view.request = request view.application = mock_application @@ -1734,6 +1736,7 @@ def test_v3_form_scopes_are_intersection_of_app_and_requested(self, mock_pc): view = AuthorizationView(version=Versions.V3) mock_application = MagicMock() request = HttpRequest() + request.session = {} request.beneficiary_name = 'Test A User' view.request = request view.application = mock_application diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index f576d0991..67c8246d2 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -1456,8 +1456,12 @@ def post(self, request, *args, **kwargs): # Restore version after logout if version is not None: request.session['version'] = version - # this is the key that the testclient looks for - request.session['api_ver'] = version + + if 'testclient' in oauth_params.get('redirect_uri', ''): + # this is the key that the testclient looks for + request.session['api_ver'] = version + request.session['code_verifier'] = oauth_params.get('code_verifier') + request.session['client_id'] = oauth_params.get('client_id') # Rebuild the authorize URL with original params base_url = request.build_absolute_uri('/').rstrip('/') diff --git a/apps/testclient/tests.py b/apps/testclient/tests.py index bd8f8cd92..8a1fd2b15 100644 --- a/apps/testclient/tests.py +++ b/apps/testclient/tests.py @@ -467,3 +467,50 @@ def test_get_oidc_discovery(self): response = self.client.get(reverse('openid-configuration')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'userinfo_endpoint') + + +@pytest.mark.django_db +def test_setup_testclient_http_response_is_post_switch_account() -> None: + # TODO: replace with fixtures once 4964/4965 branch is merged + call_command('create_blue_button_scopes') + call_command('create_test_user_and_application') + result = setup_testclient_http_response(version=3, post_switch_account_link=True) + + assert 'code_challenge_method' not in result.keys() + assert 'code_verifier' not in result.keys() + assert 'code_challenge' not in result.keys() + assert 'state' not in result.keys() + assert 'redirect_uri' not in result.keys() + + assert 'resource_uri' in result.keys() + assert 'coverage_uri' in result.keys() + assert 'authorization_uri' in result.keys() + assert 'token_uri' in result.keys() + assert 'userinfo_uri' in result.keys() + assert 'patient_uri' in result.keys() + assert 'eob_uri' in result.keys() + assert 'coverage_uri' in result.keys() + assert 'digital_insurance_card_uri' in result.keys() + + +@pytest.mark.django_db +def test_setup_testclient_http_response_not_post_switch_account() -> None: + # TODO: replace with fixtures once 4964/4965 branch is merged + call_command('create_blue_button_scopes') + call_command('create_test_user_and_application') + result = setup_testclient_http_response(version=3, post_switch_account_link=False) + + assert 'code_challenge_method' in result.keys() + assert 'code_verifier' in result.keys() + assert 'code_challenge' in result.keys() + assert 'state' in result.keys() + assert 'redirect_uri' in result.keys() + assert 'resource_uri' in result.keys() + assert 'coverage_uri' in result.keys() + assert 'authorization_uri' in result.keys() + assert 'token_uri' in result.keys() + assert 'userinfo_uri' in result.keys() + assert 'patient_uri' in result.keys() + assert 'eob_uri' in result.keys() + assert 'coverage_uri' in result.keys() + assert 'digital_insurance_card_uri' in result.keys() diff --git a/apps/testclient/utils.py b/apps/testclient/utils.py index 50979f02c..63dfe7263 100644 --- a/apps/testclient/utils.py +++ b/apps/testclient/utils.py @@ -36,7 +36,9 @@ def _start_url_with_http_or_https(host: str) -> str: def setup_testclient_http_response( - include_client_secret: bool = True, version: int = Versions.NOT_AN_API_VERSION + include_client_secret: bool = True, + version: int = Versions.NOT_AN_API_VERSION, + post_switch_account_link: bool = True, ) -> OrderedDict: """Prepare testclient response environment @@ -48,6 +50,8 @@ def setup_testclient_http_response( Args: include_client_secret (bool) : What it says. version (Version): Which version of the API are we navigating through. + post_switch_account_link (bool): Flag for if we are hitting this function after the Switch account link + on the v3 permissions screen was clicked. If so, we only want to set the resource uri values Returns: OrderedDict: A dictionary used to prepare/extend the Django session. @@ -66,14 +70,15 @@ def setup_testclient_http_response( host = _start_url_with_http_or_https(host) response['resource_uri'] = host - response['redirect_uri'] = '{}{}'.format(host, TESTCLIENT_REDIRECT_URI) response['coverage_uri'] = '{}/v{}/fhir/Coverage/'.format(host, version) - auth_data = __generate_auth_data() - response['code_challenge_method'] = 'S256' - response['code_verifier'] = auth_data['code_verifier'] - response['code_challenge'] = auth_data['code_challenge'] - response['state'] = auth_data['state'] + if not post_switch_account_link: + auth_data = __generate_auth_data() + response['code_challenge_method'] = 'S256' + response['code_verifier'] = auth_data['code_verifier'] + response['code_challenge'] = auth_data['code_challenge'] + response['state'] = auth_data['state'] + response['redirect_uri'] = '{}{}'.format(host, TESTCLIENT_REDIRECT_URI) response['authorization_uri'] = f'{host}/v{version}/o/authorize/' response['token_uri'] = f'{host}/v{version}/o/token/' diff --git a/apps/testclient/views.py b/apps/testclient/views.py index 4ed879361..7169857b3 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -105,8 +105,10 @@ def _get_oauth2_session_with_redirect(request: HttpRequest) -> OAuth2Session: client_id = request.session['client_id'] else: client_id = request.session['auth_client_id'] - if request.session.get('redirect_uri'): - redirect_uri = request.session['redirect_uri'] + if request.session.get('redirect_uri') or request.session.get('oauth_params', {}).get('redirect_uri'): + redirect_uri = request.session.get('redirect_uri') or request.session.get('oauth_params', {}).get( + 'redirect_uri' + ) else: redirect_uri = 'http://localhost:8000/mymedicare/sls-callback' return OAuth2Session(client_id, redirect_uri=redirect_uri) @@ -202,13 +204,10 @@ def callback(request: HttpRequest): # It is not clear, now (2025) why it should be '' instead of `None`. # Perhaps oas.fetch_token fails (and raises a `MissingTokenError`) if the code verifier # cannot be pulled from the session. - cv = request.session.get('code_verifier', '') - - # This conditional was an attempt to retrieve the code_verifier, but it is no - # longer available on the request at this point, not sure why if not cv: - cv = request.session.get('oauth_params', {}).get('code_verifier', '') + session_cache = getattr(request.session, '_session_cache', {}) + cv = session_cache.get('code_verifier', '') token = oas.fetch_token( token_uri, client_secret=get_client_secret(), authorization_response=auth_uri, code_verifier=cv @@ -240,7 +239,12 @@ def callback(request: HttpRequest): # We are guaranteed at this point that the patient_id is not None, and it is a synthetic user id. request.session['token'] = token - userinfo_uri = request.session['userinfo_uri'] + + userinfo_uri = request.session.get('userinfo_uri') + # If there is no userinfo_uri in the session, that means the Switch account link on the v3 permissions screen + # was clicked, and we need to reset certain uri values, so the testclient works correctly + if not userinfo_uri: + request.session.update(setup_testclient_http_response(version=version, post_switch_account_link=True)) try: userinfo = oas.get(userinfo_uri).json() @@ -327,7 +331,7 @@ def _link_session_or_version_is_bad(session, version): def _authorize_link(request: HttpRequest, version=Versions.NOT_AN_API_VERSION): - request.session.update(setup_testclient_http_response(version=version)) + request.session.update(setup_testclient_http_response(version=version, post_switch_account_link=False)) oas = _get_oauth2_session_with_redirect(request) # We need scopes in V3. diff --git a/templates/design_system/authorize_v3.html b/templates/design_system/authorize_v3.html index 854cfe317..a57bbafa0 100644 --- a/templates/design_system/authorize_v3.html +++ b/templates/design_system/authorize_v3.html @@ -40,7 +40,7 @@ display: flex !important; align-items: center; background-color: white; - padding: 16px; + padding: 16px 16px 8px 16px; } /* Two-line heading inside the button */ @@ -138,19 +138,6 @@

{% trans "Hi, "%}{{ beneficiary_name }}.

-
- {% trans "Not you? " %} -
- {% csrf_token %} - -
-
-
{% trans "Not you? " %}
@@ -257,7 +244,7 @@

aria-labelledby="accordion-btn-1" hidden > -

Medicare coverage data, if applicable:

+ Medicare coverage data, if applicable:
  • Medicaid dual eligibility status
  • Medicaid state and eligibility date
  • @@ -304,7 +291,7 @@

    aria-labelledby="accordion-btn-2" hidden > -

    Medicare claims data, if applicable:

    + Medicare claims data, if applicable:
    • Provider information (provider ID number, name, specialty)
    • Diagnosis information (diagnosis codes, related diagnosis group code, condition present on admission)
    • @@ -356,7 +343,7 @@

      aria-labelledby="accordion-btn-3" hidden > -

      Alcohol and substance abuse claims, if applicable:

      + Alcohol and substance abuse claims, if applicable:
      • Diagnoses (alcohol or drug dependence, harmful use, drug misuse, withdrawal, overdose, complications caused by substance use)
      • Services (alcohol and drug screening, brief interventions, counseling/therapy, and related treatment services)
      • @@ -374,7 +361,7 @@

        {{ form.non_field_errors }}
        -

        +

        Icons/Help @@ -387,6 +374,8 @@

        + + {% if application.support_email %} {% trans "Review " %}{{ application.name }}{% trans "'s " %} Privacy Policy @@ -427,6 +416,46 @@

        {{ application.support_email }}. {% endif %} + {% else %} + + {% trans "Review " %}{{ application.name }}{% trans "'s " %} + + Privacy Policy + + + + {% trans " and " %} + + + Terms and Conditions + + to learn how they manage your data. + + + {% endif %} +

        From 5aae555722cb0f9f94377513370de4c58d86b4ea Mon Sep 17 00:00:00 2001 From: James Demery Date: Wed, 8 Jul 2026 15:47:15 -0400 Subject: [PATCH 05/10] Remove unused code --- apps/dot_ext/utils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/apps/dot_ext/utils.py b/apps/dot_ext/utils.py index b92ba8ac5..8b9fdc5ab 100644 --- a/apps/dot_ext/utils.py +++ b/apps/dot_ext/utils.py @@ -1,5 +1,3 @@ -import base64 -import hashlib import logging import os import re @@ -411,12 +409,3 @@ def check_auth_tracking_and_create_access_token_extension( AccessTokenExtension.objects.get_or_create( access_token=token, include_samhsa=include_samhsa, part_d_eob_only=prior_part_d_eob_only ) - - -def generate_code_verifier() -> str: - return base64.urlsafe_b64encode(os.urandom(44)).rstrip(b'=').decode('utf-8') - - -def generate_code_challenge(code_verifier: str) -> str: - code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode('ASCII')).digest()).decode('utf-8') - return code_challenge From c75dba49105aaf90d20a573e2aee2fbeff22cc94 Mon Sep 17 00:00:00 2001 From: James Demery Date: Thu, 9 Jul 2026 08:45:21 -0400 Subject: [PATCH 06/10] Cleanup tests, add a comment, add a style class --- apps/testclient/tests.py | 90 +++++++++++++---------- apps/testclient/views.py | 6 +- templates/design_system/authorize_v3.html | 12 ++- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/apps/testclient/tests.py b/apps/testclient/tests.py index 8a1fd2b15..f96173b54 100644 --- a/apps/testclient/tests.py +++ b/apps/testclient/tests.py @@ -1,5 +1,6 @@ import json import os +from typing import List import pytest from django.core.management import call_command @@ -470,47 +471,58 @@ def test_get_oidc_discovery(self): @pytest.mark.django_db -def test_setup_testclient_http_response_is_post_switch_account() -> None: +@pytest.mark.parametrize( + 'post_switch_account_link, expected_keys, expected_excluded_keys', + [ + ( + True, + [ + 'resource_uri', + 'coverage_uri', + 'authorization_uri', + 'token_uri', + 'userinfo_uri', + 'patient_uri', + 'eob_uri', + 'coverage_uri', + 'digital_insurance_card_uri', + ], + ['code_challenge_method', 'code_verifier', 'code_challenge', 'state', 'redirect_uri'], + ), + ( + False, + [ + 'resource_uri', + 'coverage_uri', + 'authorization_uri', + 'token_uri', + 'userinfo_uri', + 'patient_uri', + 'eob_uri', + 'coverage_uri', + 'digital_insurance_card_uri', + 'code_challenge_method', + 'code_verifier', + 'code_challenge', + 'state', + 'redirect_uri', + ], + [], + ), + ], +) +def test_setup_testclient_http_response_is_post_switch_account( + post_switch_account_link: bool, + expected_keys: List[str], + expected_excluded_keys: List[str], +) -> None: # TODO: replace with fixtures once 4964/4965 branch is merged call_command('create_blue_button_scopes') call_command('create_test_user_and_application') - result = setup_testclient_http_response(version=3, post_switch_account_link=True) + result = setup_testclient_http_response(version=3, post_switch_account_link=post_switch_account_link) - assert 'code_challenge_method' not in result.keys() - assert 'code_verifier' not in result.keys() - assert 'code_challenge' not in result.keys() - assert 'state' not in result.keys() - assert 'redirect_uri' not in result.keys() + for key in expected_excluded_keys: + assert key not in result.keys() - assert 'resource_uri' in result.keys() - assert 'coverage_uri' in result.keys() - assert 'authorization_uri' in result.keys() - assert 'token_uri' in result.keys() - assert 'userinfo_uri' in result.keys() - assert 'patient_uri' in result.keys() - assert 'eob_uri' in result.keys() - assert 'coverage_uri' in result.keys() - assert 'digital_insurance_card_uri' in result.keys() - - -@pytest.mark.django_db -def test_setup_testclient_http_response_not_post_switch_account() -> None: - # TODO: replace with fixtures once 4964/4965 branch is merged - call_command('create_blue_button_scopes') - call_command('create_test_user_and_application') - result = setup_testclient_http_response(version=3, post_switch_account_link=False) - - assert 'code_challenge_method' in result.keys() - assert 'code_verifier' in result.keys() - assert 'code_challenge' in result.keys() - assert 'state' in result.keys() - assert 'redirect_uri' in result.keys() - assert 'resource_uri' in result.keys() - assert 'coverage_uri' in result.keys() - assert 'authorization_uri' in result.keys() - assert 'token_uri' in result.keys() - assert 'userinfo_uri' in result.keys() - assert 'patient_uri' in result.keys() - assert 'eob_uri' in result.keys() - assert 'coverage_uri' in result.keys() - assert 'digital_insurance_card_uri' in result.keys() + for key in expected_keys: + assert key in result.keys() diff --git a/apps/testclient/views.py b/apps/testclient/views.py index 7169857b3..ac49ce97d 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -110,7 +110,11 @@ def _get_oauth2_session_with_redirect(request: HttpRequest) -> OAuth2Session: 'redirect_uri' ) else: - redirect_uri = 'http://localhost:8000/mymedicare/sls-callback' + # Default to /testclient/callback if there is no redirect_uri attribute. Confirmed that HOSTNAME_URL + # has no trailing slash in any deployed env. + host = _start_url_with_http_or_https(settings.HOSTNAME_URL) + redirect_uri = host + '/testclient/callback' + return OAuth2Session(client_id, redirect_uri=redirect_uri) diff --git a/templates/design_system/authorize_v3.html b/templates/design_system/authorize_v3.html index a57bbafa0..586b25a41 100644 --- a/templates/design_system/authorize_v3.html +++ b/templates/design_system/authorize_v3.html @@ -63,6 +63,12 @@ font-size: 14px; line-height: 150%; } +.ds-c-accordion__button-line1_sub_header { + font-weight: 600; + font-size: 16px; + padding-bottom: 0px; +} + .ds-c-accordion__item{ border: 1px solid #D9D9D9; border-radius: 8px; @@ -244,7 +250,7 @@

        aria-labelledby="accordion-btn-1" hidden > - Medicare coverage data, if applicable: + Medicare coverage data, if applicable:
        • Medicaid dual eligibility status
        • Medicaid state and eligibility date
        • @@ -291,7 +297,7 @@

          aria-labelledby="accordion-btn-2" hidden > - Medicare claims data, if applicable: + Medicare claims data, if applicable:
          • Provider information (provider ID number, name, specialty)
          • Diagnosis information (diagnosis codes, related diagnosis group code, condition present on admission)
          • @@ -343,7 +349,7 @@

            aria-labelledby="accordion-btn-3" hidden > - Alcohol and substance abuse claims, if applicable: + Alcohol and substance abuse claims, if applicable:
            • Diagnoses (alcohol or drug dependence, harmful use, drug misuse, withdrawal, overdose, complications caused by substance use)
            • Services (alcohol and drug screening, brief interventions, counseling/therapy, and related treatment services)
            • From c3a96cca127bb8945a68a036e3443dd8b408bd6f Mon Sep 17 00:00:00 2001 From: James Demery Date: Thu, 9 Jul 2026 11:22:23 -0400 Subject: [PATCH 07/10] Update color --- templates/design_system/authorize_v3.html | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/design_system/authorize_v3.html b/templates/design_system/authorize_v3.html index 586b25a41..33e881c6c 100644 --- a/templates/design_system/authorize_v3.html +++ b/templates/design_system/authorize_v3.html @@ -67,6 +67,7 @@ font-weight: 600; font-size: 16px; padding-bottom: 0px; + color: #404040; } .ds-c-accordion__item{ From c118c3b80c55dbb38fdad6c2dd9471ef1865d06f Mon Sep 17 00:00:00 2001 From: Brandon Wang Date: Fri, 10 Jul 2026 12:57:00 -0500 Subject: [PATCH 08/10] adding a get_oauth_param helper to simplify param retrieval --- apps/dot_ext/utils.py | 17 +++++++++++++++++ apps/dot_ext/views/authorization.py | 21 +++++++-------------- apps/testclient/views.py | 13 ++++--------- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/apps/dot_ext/utils.py b/apps/dot_ext/utils.py index e06a9dda0..eca4517e0 100644 --- a/apps/dot_ext/utils.py +++ b/apps/dot_ext/utils.py @@ -56,6 +56,23 @@ def validate_client_id(client_id: str) -> None: ) +def get_oauth_param(request: HttpRequest, parameter: str, fallback_session_parameter: str = None) -> str | None: + """Resolve an OAuth parameter from GET, then session, then session['oauth_params']. + + Args: + request: Django HttpRequest object + parameter: The parameter name to look up + fallback_session_parameter: Optional alternate session key to try (e.g. 'auth_client_id' for 'client_id') + + Returns: + The first truthy value found, or None + """ + result = request.GET.get(parameter) or request.session.get(parameter) + if not result and fallback_session_parameter: + result = request.session.get(fallback_session_parameter) + return result or request.session.get('oauth_params', {}).get(parameter) + + def remove_application_user_pair_tokens_data_access( application, user, delete_data_access_grant: bool, delete_access_tokens: bool ): diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index 67c8246d2..21bbbcfe9 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -108,6 +108,7 @@ from apps.dot_ext.utils import ( check_auth_tracking_and_create_access_token_extension, get_api_version_number_from_url, + get_oauth_param, json_response_from_oauth2_error, remove_application_user_pair_tokens_data_access, validate_app_is_active, @@ -1425,21 +1426,13 @@ def post(self, request, *args, **kwargs): # Save the original OAuth params before logout oauth_params = { - 'client_id': request.GET.get('client_id') - or request.session.get('client_id') - or request.session.get('auth_client_id'), - 'redirect_uri': request.GET.get('redirect_uri') - or request.session.get('redirect_uri') - or request.session['oauth_params'].get('redirect_uri'), - 'response_type': request.GET.get('response_type') - or request.session.get('response_type') - or request.session['oauth_params'].get('response_type'), - 'state': request.GET.get('state') - or request.session.get('state') - or request.session['oauth_params'].get('state'), + 'client_id': get_oauth_param(request, 'client_id', 'auth_client_id'), + 'redirect_uri': get_oauth_param(request, 'redirect_uri'), + 'response_type': get_oauth_param(request, 'response_type'), + 'state': get_oauth_param(request, 'state'), 'code_challenge_method': CODE_CHALLENGE_METHOD_S256, - 'code_challenge': request.session['oauth_params'].get('code_challenge'), - 'scope': request.session['oauth_params'].get('scope'), + 'code_challenge': request.session.get('oauth_params', {}).get('code_challenge'), + 'scope': request.session.get('oauth_params', {}).get('scope'), 'code_verifier': request.session.get('code_verifier'), } diff --git a/apps/testclient/views.py b/apps/testclient/views.py index ac49ce97d..fe0ce1936 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -17,6 +17,7 @@ from apps.constants import HHS_SERVER_LOGNAME_FMT from apps.dot_ext.loggers import cleanup_session_auth_flow_trace +from apps.dot_ext.utils import get_oauth_param from apps.fhir.bluebutton.views.home import fhir_conformance_v1, fhir_conformance_v2, fhir_conformance_v3 from apps.testclient.constants import HOME_PAGE, RESULTS_PAGE, EndpointUrl, ResponseErrors from apps.testclient.utils import ( @@ -101,15 +102,9 @@ def _convert_response_string_to_json(json_response: str) -> Dict[str, object]: def _get_oauth2_session_with_redirect(request: HttpRequest) -> OAuth2Session: - if request.session.get('client_id'): - client_id = request.session['client_id'] - else: - client_id = request.session['auth_client_id'] - if request.session.get('redirect_uri') or request.session.get('oauth_params', {}).get('redirect_uri'): - redirect_uri = request.session.get('redirect_uri') or request.session.get('oauth_params', {}).get( - 'redirect_uri' - ) - else: + client_id = get_oauth_param(request, 'client_id', 'auth_client_id') + redirect_uri = get_oauth_param(request, 'redirect_uri') + if not redirect_uri: # Default to /testclient/callback if there is no redirect_uri attribute. Confirmed that HOSTNAME_URL # has no trailing slash in any deployed env. host = _start_url_with_http_or_https(settings.HOSTNAME_URL) From 6a5b091ab144e5da5a84c9e7d5b1fd9eadd7eeaf Mon Sep 17 00:00:00 2001 From: Brandon Wang Date: Fri, 10 Jul 2026 15:46:37 -0500 Subject: [PATCH 09/10] cleaning up oauth_params retrieval --- apps/dot_ext/views/authorization.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index 21bbbcfe9..76a436efe 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -266,15 +266,19 @@ def get_context_data(self, **kwargs): context['permission_end_date_text'] = self.application.access_end_date_text() context['permission_end_date'] = self.application.access_end_date() - self.request.session['oauth_params'] = { - 'client_id': self.request.GET.get('client_id'), - 'redirect_uri': self.request.GET.get('redirect_uri'), - 'response_type': self.request.GET.get('response_type'), - 'scope': self.request.GET.get('scope'), - 'state': self.request.GET.get('state'), - 'code_challenge': self.request.GET.get('code_challenge'), - 'code_challenge_method': self.request.GET.get('code_challenge_method'), - } + params = [ + 'client_id', + 'redirect_uri', + 'response_type', + 'scope', + 'state', + 'code_challenge', + 'code_challenge_method', + ] + oauth_params = {} + for param in params: + oauth_params[param] = self.request.GET.get(param) + self.request.session['oauth_params'] = oauth_params if 'form' in context and self.version == Versions.V3: # By setting this to matching_scopes instead of application_scopes, we ensure that the scopes # for the access token are in the intersection of what the application is allowed to have and From 2bcf1ad5cb1b711b59cf344b8c09d62f5c6bb197 Mon Sep 17 00:00:00 2001 From: James Demery Date: Tue, 14 Jul 2026 12:38:55 -0400 Subject: [PATCH 10/10] Apply suggestion from @annamontare-nava Co-authored-by: annamontare-nava <267455234+annamontare-nava@users.noreply.github.com> --- apps/testclient/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/testclient/views.py b/apps/testclient/views.py index fe0ce1936..aedd0e66f 100644 --- a/apps/testclient/views.py +++ b/apps/testclient/views.py @@ -149,7 +149,7 @@ def _is_synthetic_patient_id(patient_id: str) -> bool: def callback(request: HttpRequest): """Called when returning from authorizing as a beneficiary. - When using the text client, users can authorize as a beneficiary as part of the auth workflow. + When using the test client, users can authorize as a beneficiary as part of the auth workflow. https://bluebutton.cms.gov/developers/#authorization:~:text=Click-,Authorize%20as%20a%20Beneficiary,-.