Skip to content

Commit 101bffb

Browse files
loganbertramgithub-advanced-security[bot]bwang-icf
authored
BB2-3321: Oauth revoke endpoint (#1251)
* Initial attempt * Added test * Added to well-known configs and fhir capability statement and in swagger. * Fix openapi yaml typo * Added 404 condition for token not found * Improved 404 and error handling * Fix code scanning alert no. 51: Reflected server-side cross-site scripting Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Empty change to kick jenkins test flake * Fix token escape * Conform to Oauth expectations better --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: bwang-icf <[email protected]>
1 parent 2ece510 commit 101bffb

File tree

9 files changed

+112
-5
lines changed

9 files changed

+112
-5
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,4 @@ by their respective authors.
167167
License
168168
-------
169169

170-
This project is free and open source software under the Apache 2 license. See LICENSE for more information.
170+
This project is free and open source software under the Apache 2 license. See LICENSE for more information.

apps/dot_ext/tests/test_authorization.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,61 @@ def test_dag_expiration_exists(self):
578578
expiration_date_string = strftime('%Y-%m-%d %H:%M:%SZ', expiration_date.timetuple())
579579
self.assertEqual(tkn["access_grant_expiration"][:-4], expiration_date_string[:-4])
580580

581+
def test_revoke_endpoint(self):
582+
redirect_uri = 'http://localhost'
583+
# create a user
584+
self._create_user('anna', '123456')
585+
capability_a = self._create_capability('Capability A', [])
586+
capability_b = self._create_capability('Capability B', [])
587+
# create an application and add capabilities
588+
application = self._create_application(
589+
'an app',
590+
grant_type=Application.GRANT_AUTHORIZATION_CODE,
591+
client_type=Application.CLIENT_CONFIDENTIAL,
592+
redirect_uris=redirect_uri)
593+
application.scope.add(capability_a, capability_b)
594+
# user logs in
595+
request = HttpRequest()
596+
self.client.login(request=request, username='anna', password='123456')
597+
# post the authorization form with only one scope selected
598+
payload = {
599+
'client_id': application.client_id,
600+
'response_type': 'code',
601+
'redirect_uri': redirect_uri,
602+
'scope': ['capability-a'],
603+
'expires_in': 86400,
604+
'allow': True,
605+
}
606+
response = self.client.post(reverse('oauth2_provider:authorize'), data=payload)
607+
self.client.logout()
608+
self.assertEqual(response.status_code, 302)
609+
# now extract the authorization code and use it to request an access_token
610+
query_dict = parse_qs(urlparse(response['Location']).query)
611+
authorization_code = query_dict.pop('code')
612+
token_request_data = {
613+
'grant_type': 'authorization_code',
614+
'code': authorization_code,
615+
'redirect_uri': redirect_uri,
616+
'client_id': application.client_id,
617+
'client_secret': application.client_secret_plain,
618+
}
619+
c = Client()
620+
response = c.post('/v1/o/token/', data=token_request_data)
621+
self.assertEqual(response.status_code, 200)
622+
# extract token and use it to make a revoke request
623+
tkn = response.json()['access_token']
624+
revoke_request_data = f"token={tkn}&client_id={application.client_id}&client_secret={application.client_secret_plain}"
625+
content_type = "application/x-www-form-urlencoded"
626+
c = Client()
627+
rev_response = c.post('/v1/o/revoke/', data=revoke_request_data, content_type=content_type)
628+
self.assertEqual(rev_response.status_code, 200)
629+
# check DAG deletion
630+
dags_count = DataAccessGrant.objects.count()
631+
self.assertEqual(dags_count, 0)
632+
# check token deletion
633+
tkn_count = AccessToken.objects.filter(token=tkn).count()
634+
self.assertEqual(tkn_count, 0)
635+
581636
def test_refresh_with_revoked_token(self):
582637
redirect_uri = 'http://localhost'
583638
# create a user

apps/dot_ext/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
),
1616
path("token/", views.TokenView.as_view(), name="token"),
1717
path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"),
18+
path("revoke/", views.RevokeView.as_view(), name="revoke"),
1819
path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"),
1920
]
2021

apps/dot_ext/v2/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
),
1616
path("token/", views.TokenView.as_view(), name="token-v2"),
1717
path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token-v2"),
18+
path("revoke/", views.RevokeView.as_view(), name="revoke-v2"),
1819
path("introspect/", views.IntrospectTokenView.as_view(), name="introspect-v2"),
1920
]
2021

apps/dot_ext/views/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
from .application import ApplicationRegistration, ApplicationUpdate, ApplicationDelete # NOQA
2-
from .authorization import AuthorizationView, ApprovalView, TokenView, RevokeTokenView, IntrospectTokenView # NOQA
2+
from .authorization import AuthorizationView, ApprovalView, TokenView, RevokeTokenView, RevokeView, IntrospectTokenView # NOQA

apps/dot_ext/views/authorization.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from oauth2_provider.models import get_application_model
2323
from oauthlib.oauth2.rfc6749.errors import InvalidClientError, InvalidGrantError
2424
from urllib.parse import urlparse, parse_qs
25-
25+
import html
2626
from apps.dot_ext.scopes import CapabilitiesScopes
2727
import apps.logging.request_logger as bb2logging
2828

@@ -120,12 +120,12 @@ def sensitive_info_check(self, request):
120120
def get_template_names(self):
121121
flag = get_waffle_flag_model().get("limit_data_access")
122122
if waffle.switch_is_active('require-scopes'):
123-
if flag.rollout or (flag.id is not None and flag.is_active_for_user(self.application.user)):
123+
if flag.rollout or (flag.id is not None and self.application and flag.is_active_for_user(self.application.user)):
124124
return ["design_system/new_authorize_v2.html"]
125125
else:
126126
return ["design_system/authorize_v2.html"]
127127
else:
128-
if flag.rollout or (flag.id is not None and flag.is_active_for_user(self.user)):
128+
if flag.rollout or (flag.id is not None and self.user and flag.is_active_for_user(self.user)):
129129
return ["design_system/new_authorize_v2.html"]
130130
else:
131131
return ["design_system/authorize.html"]
@@ -354,6 +354,40 @@ def post(self, request, *args, **kwargs):
354354
return super().post(request, args, kwargs)
355355

356356

357+
@method_decorator(csrf_exempt, name="dispatch")
358+
class RevokeView(DotRevokeTokenView):
359+
360+
@method_decorator(sensitive_post_parameters("password"))
361+
def post(self, request, *args, **kwargs):
362+
at_model = get_access_token_model()
363+
try:
364+
app = validate_app_is_active(request)
365+
except (InvalidClientError, InvalidGrantError) as error:
366+
return json_response_from_oauth2_error(error)
367+
368+
tkn = request.POST.get('token')
369+
if tkn is not None:
370+
escaped_tkn = html.escape(tkn)
371+
else:
372+
escaped_tkn = ""
373+
374+
try:
375+
token = at_model.objects.get(token=tkn)
376+
except at_model.DoesNotExist:
377+
log.debug(f"Token {escaped_tkn} was not found.")
378+
379+
try:
380+
dag = DataAccessGrant.objects.get(
381+
beneficiary=token.user,
382+
application=app
383+
)
384+
dag.delete()
385+
except Exception:
386+
log.debug(f"DAG lookup failed for token {escaped_tkn}.")
387+
388+
return super().post(request, args, kwargs)
389+
390+
357391
@method_decorator(csrf_exempt, name="dispatch")
358392
class IntrospectTokenView(DotIntrospectTokenView):
359393

apps/fhir/bluebutton/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,12 +636,16 @@ def build_oauth_resource(request, v2=False, format_type="json"):
636636
<extension url="authorize">
637637
<valueUri>%s</valueUri>
638638
</extension>
639+
<extension url="revoke">
640+
<valueUri>%s</valueUri>
641+
</extension>
639642
</extension>
640643
641644
</security>
642645
""" % (
643646
endpoints["token_endpoint"],
644647
endpoints["authorization_endpoint"],
648+
endpoints["revocation_endpoint"],
645649
)
646650

647651
else: # json
@@ -680,6 +684,7 @@ def build_oauth_resource(request, v2=False, format_type="json"):
680684
"url": "authorize",
681685
"valueUri": endpoints["authorization_endpoint"],
682686
},
687+
{"url": "revoke", "valueUri": endpoints["revocation_endpoint"]},
683688
],
684689
}
685690
]

apps/wellknown/views/openid.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def build_endpoint_info(data=OrderedDict(), v2=False, issuer=""):
6161
data["issuer"] = issuer
6262
data["authorization_endpoint"] = issuer + \
6363
reverse('oauth2_provider:authorize' if not v2 else 'oauth2_provider_v2:authorize-v2')
64+
data["revocation_endpoint"] = issuer + reverse('oauth2_provider:revoke')
6465
data["token_endpoint"] = issuer + \
6566
reverse('oauth2_provider:token' if not v2 else 'oauth2_provider_v2:token-v2')
6667
data["userinfo_endpoint"] = issuer + \

static/openapi.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,8 @@ components:
717717
type: string
718718
authorization_endpoint:
719719
type: string
720+
revocation_endpoint:
721+
type: string
720722
token_endpoint:
721723
type: string
722724
userinfo_endpoint:
@@ -746,6 +748,8 @@ components:
746748
type: string
747749
authorization_endpoint:
748750
type: string
751+
revocation_endpoint:
752+
type: string
749753
token_endpoint:
750754
type: string
751755
userinfo_endpoint:
@@ -812,6 +816,7 @@ components:
812816
value:
813817
issuer: 'https://sandbox.bluebutton.cms.gov'
814818
authorization_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/o/authorize/'
819+
revocation_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/o/revoke/'
815820
token_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/o/token/'
816821
userinfo_endpoint: 'https://sandbox.bluebutton.cms.gov/v1/connect/userinfo'
817822
ui_locales_supported:
@@ -830,6 +835,7 @@ components:
830835
value:
831836
issuer: 'https://sandbox.bluebutton.cms.gov'
832837
authorization_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/o/authorize/'
838+
revocation_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/o/revoke/'
833839
token_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/o/token/'
834840
userinfo_endpoint: 'https://sandbox.bluebutton.cms.gov/v2/connect/userinfo'
835841
ui_locales_supported:
@@ -1230,6 +1236,8 @@ components:
12301236
valueUri: 'https://sandbox.bluebutton.cms.gov/v1/o/token/'
12311237
- url: authorize
12321238
valueUri: 'https://sandbox.bluebutton.cms.gov/v1/o/authorize/'
1239+
- url: revoke
1240+
valueUri: 'https://sandbox.bluebutton.cms.gov/v1/o/revoke/'
12331241

12341242
V2FhirMetadataExample:
12351243
value:
@@ -1665,6 +1673,8 @@ components:
16651673
valueUri: 'https://sandbox.bluebutton.cms.gov/v2/o/token/'
16661674
- url: authorize
16671675
valueUri: 'https://sandbox.bluebutton.cms.gov/v2/o/authorize/'
1676+
- url: revoke
1677+
valueUri: 'https://sandbox.bluebutton.cms.gov/v2/o/revoke/'
16681678

16691679
V1UserInfoExample:
16701680
value:

0 commit comments

Comments
 (0)