Skip to content

Commit 53330c1

Browse files
authored
Merge branch 'master' into loganbertram/BB2-3484-scope-incident-fix
2 parents ca9b7a2 + 101bffb commit 53330c1

File tree

20 files changed

+454
-15
lines changed

20 files changed

+454
-15
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
@@ -23,7 +23,7 @@
2323
from oauth2_provider.models import get_application_model
2424
from oauthlib.oauth2.rfc6749.errors import InvalidClientError, InvalidGrantError
2525
from urllib.parse import urlparse, parse_qs
26-
26+
import html
2727
from apps.dot_ext.scopes import CapabilitiesScopes
2828
import apps.logging.request_logger as bb2logging
2929

@@ -121,12 +121,12 @@ def sensitive_info_check(self, request):
121121
def get_template_names(self):
122122
flag = get_waffle_flag_model().get("limit_data_access")
123123
if waffle.switch_is_active('require-scopes'):
124-
if flag.rollout or (flag.id is not None and flag.is_active_for_user(self.application.user)):
124+
if flag.rollout or (flag.id is not None and self.application and flag.is_active_for_user(self.application.user)):
125125
return ["design_system/new_authorize_v2.html"]
126126
else:
127127
return ["design_system/authorize_v2.html"]
128128
else:
129-
if flag.rollout or (flag.id is not None and flag.is_active_for_user(self.user)):
129+
if flag.rollout or (flag.id is not None and self.user and flag.is_active_for_user(self.user)):
130130
return ["design_system/new_authorize_v2.html"]
131131
else:
132132
return ["design_system/authorize.html"]
@@ -362,6 +362,40 @@ def post(self, request, *args, **kwargs):
362362
return super().post(request, args, kwargs)
363363

364364

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

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
]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import re
2+
import logging
3+
import logging.config
4+
5+
MBI_WITH_HYPHEN_PATTERN = r"""\b
6+
[1-9](?![SLOIBZsloibz])[A-Za-z](?![SLOIBZsloibz)])[A-Za-z\d]\d
7+
-(?![SLOIBZsloibz])[A-Za-z](?![SLOIBZsloibz])[A-Za-z\d]\d
8+
-((?![SLOIBZsloibz])[A-Za-z]){2}\d{2}
9+
\b
10+
"""
11+
12+
MBI_WITHOUT_HYPHEN_PATTERN = r"""\b
13+
[1-9](?![SLOIBZsloibz])[A-Za-z](?![SLOIBZsloibz)])[A-Za-z\d]\d
14+
(?![SLOIBZsloibz])[A-Za-z](?![SLOIBZsloibz])[A-Za-z\d]\d
15+
((?![SLOIBZsloibzd])[A-Za-z]){2}\d{2}
16+
\b"""
17+
18+
MBI_PATTERN = f'({MBI_WITH_HYPHEN_PATTERN}|{MBI_WITHOUT_HYPHEN_PATTERN})'
19+
SENSITIVE_DATA_FILTER = "sensitive_data_filter"
20+
21+
22+
def mask_if_has_mbi(text):
23+
return re.sub(MBI_PATTERN, '***MBI***', str(text), flags=re.VERBOSE)
24+
25+
26+
def mask_mbi(value_to_mask):
27+
if isinstance(value_to_mask, str):
28+
return mask_if_has_mbi(value_to_mask)
29+
30+
if isinstance(value_to_mask, tuple):
31+
return tuple([mask_if_has_mbi(arg) for arg in value_to_mask])
32+
33+
if isinstance(value_to_mask, list):
34+
return [mask_if_has_mbi(arg) for arg in value_to_mask]
35+
36+
if isinstance(value_to_mask, dict):
37+
for key, value in value_to_mask.items():
38+
value_to_mask[key] = mask_mbi(value)
39+
40+
return value_to_mask
41+
42+
43+
class SensitiveDataFilter(logging.Filter):
44+
45+
def filter(self, record):
46+
try:
47+
record.args = mask_mbi(record.args)
48+
record.msg = mask_mbi(record.msg)
49+
return True
50+
except Exception:
51+
pass

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 + \

hhs_oauth_server/settings/base.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from apps.logging.sensitive_logging_filters import SENSITIVE_DATA_FILTER, SensitiveDataFilter
23
import dj_database_url
34
import socket
45
import datetime
@@ -377,6 +378,12 @@
377378
"console": {
378379
"class": "logging.StreamHandler",
379380
"formatter": "verbose",
381+
"filters": [SENSITIVE_DATA_FILTER],
382+
}
383+
},
384+
"filters": {
385+
"sensitive_data_filter": {
386+
"()": SensitiveDataFilter,
380387
}
381388
},
382389
"loggers": {
@@ -421,6 +428,10 @@
421428
"handlers": ["console"],
422429
"level": "INFO",
423430
},
431+
'django': {
432+
'handlers': ['console'],
433+
'level': 'INFO',
434+
},
424435
},
425436
},
426437
)

0 commit comments

Comments
 (0)