Skip to content

Commit ce1e3bc

Browse files
JamesDemeryNavaryan-morosa
authored andcommitted
BB2-4781: Revoke prior tokens on new auth flow (#1605)
* BB2-4781: Revoke prior tokens on new auth flow * Ensure token revocation only happens on v3 auth flows * Move the revocation of prior tokens to form_valid of AuthorizationView from post of TokenView. Revoke all previous tokens as the newest one will not have been created yet at that point * Address PR feedback, remove no longer needed test * Address feedback - use previously existing utils function * Fix typo * Add unit test coverage
1 parent 3461869 commit ce1e3bc

8 files changed

Lines changed: 166 additions & 90 deletions

File tree

apps/dot_ext/constants.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,9 @@
339339
# Result:
340340
'result_has_error': False,
341341
'result_token_scopes_granted': APPLICATION_SCOPES_NON_DEMOGRAPHIC,
342-
'result_access_token_count': 2,
343-
'result_refresh_token_count': 2,
344-
'result_archived_token_count': 2,
342+
'result_access_token_count': 1,
343+
'result_refresh_token_count': 1,
344+
'result_archived_token_count': 3,
345345
'result_archived_data_access_grant_count': 2,
346346
},
347347
'test 5: app_requires = True bene_share = False': {
@@ -365,9 +365,9 @@
365365
# Result:
366366
'result_has_error': False,
367367
'result_token_scopes_granted': APPLICATION_SCOPES_FULL,
368-
'result_access_token_count': 2,
369-
'result_refresh_token_count': 2,
370-
'result_archived_token_count': 4,
368+
'result_access_token_count': 1,
369+
'result_refresh_token_count': 1,
370+
'result_archived_token_count': 5,
371371
'result_archived_data_access_grant_count': 3,
372372
},
373373
# Tests for request_app_requires_demographic = False
@@ -419,9 +419,9 @@
419419
# Result:
420420
'result_has_error': False,
421421
'result_token_scopes_granted': SCOPES_JUST_EOB_AND_B,
422-
'result_access_token_count': 2,
423-
'result_refresh_token_count': 2,
424-
'result_archived_token_count': 8,
422+
'result_access_token_count': 1,
423+
'result_refresh_token_count': 1,
424+
'result_archived_token_count': 9,
425425
'result_archived_data_access_grant_count': 6,
426426
},
427427
'test 11: app_requires = None bene_share = None request just PATIENT and A': {
@@ -445,9 +445,9 @@
445445
# Result:
446446
'result_has_error': False,
447447
'result_token_scopes_granted': SCOPES_JUST_PATIENT_AND_A,
448-
'result_access_token_count': 2,
449-
'result_refresh_token_count': 2,
450-
'result_archived_token_count': 10,
448+
'result_access_token_count': 1,
449+
'result_refresh_token_count': 1,
450+
'result_archived_token_count': 11,
451451
'result_archived_data_access_grant_count': 7,
452452
},
453453
'test 13: app_requires = False bene_share = True request just PATIENT and A': {

apps/dot_ext/tests/test_beneficiary_demographic_scope_changes.py

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import json
2-
from apps.test import BaseApiTest
3-
from django.core.management import call_command
4-
from django.http import HttpRequest
5-
from django.urls import reverse
2+
from http import HTTPStatus
3+
from unittest import mock
64

75
# from oauth2_provider.compat import parse_qs, urlparse
86
from urllib.parse import parse_qs, urlparse
7+
8+
from django.core.management import call_command
9+
from django.http import HttpRequest
10+
from django.urls import reverse
911
from oauth2_provider.models import AccessToken, RefreshToken
1012
from rest_framework.test import APIClient
1113
from waffle.testutils import override_switch
12-
from apps.authorization.models import DataAccessGrant, ArchivedDataAccessGrant
13-
from apps.dot_ext.models import ArchivedToken, Application
14-
from http import HTTPStatus
15-
from unittest import mock
14+
15+
from apps.authorization.models import ArchivedDataAccessGrant, DataAccessGrant
16+
from apps.dot_ext.models import Application, ArchivedToken
17+
from apps.test import BaseApiTest
1618

1719

1820
class TestBeneficiaryDemographicScopesChanges(BaseApiTest):
@@ -127,7 +129,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
127129
)
128130

129131
# Assert auth request was successful
130-
self.assertEqual(status_code, 200)
132+
self.assertEqual(status_code, HTTPStatus.OK)
131133

132134
# Assert scope in response content
133135
self.assertEqual(response_scopes, sorted(APPLICATION_SCOPES_FULL))
@@ -138,7 +140,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
138140
# Assert access to userinfo end point?
139141
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_1.token)
140142
response = client.get('/v1/connect/userinfo')
141-
self.assertEqual(response.status_code, 200)
143+
self.assertEqual(response.status_code, HTTPStatus.OK)
142144

143145
# ------ TEST #2: Test refresh of token_1
144146
refresh_request_data = {
@@ -152,7 +154,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
152154
content = json.loads(response.content.decode('utf-8'))
153155

154156
# Assert successful
155-
self.assertEqual(response.status_code, 200)
157+
self.assertEqual(response.status_code, HTTPStatus.OK)
156158

157159
# Assert response scopes
158160
response_scopes = sorted(content['scope'].split())
@@ -166,7 +168,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
166168
# Assert access to userinfo end point?
167169
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token.token)
168170
response = client.get('/v1/connect/userinfo')
169-
self.assertEqual(response.status_code, 200)
171+
self.assertEqual(response.status_code, HTTPStatus.OK)
170172

171173
# Verify token counts expected.
172174
self.assertEqual(AccessToken.objects.count(), 1)
@@ -197,7 +199,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
197199
# Assert NO access to userinfo end point?
198200
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_3.token)
199201
response = client.get('/v1/connect/userinfo')
200-
self.assertEqual(response.status_code, 403)
202+
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
201203

202204
# Verify token counts expected.
203205
self.assertEqual(AccessToken.objects.count(), 1)
@@ -215,7 +217,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
215217
# Test access to userinfo end point? NO ACCESS!
216218
response = client.get('/v1/connect/userinfo')
217219
content = json.loads(response.content)
218-
self.assertEqual(response.status_code, 401)
220+
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
219221
self.assertEqual(content.get('detail', None), 'Authentication credentials were not provided.')
220222

221223
# ------ TEST #5: Test token_1 from TEST #1 token refresh? NO ACCESS!
@@ -241,7 +243,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
241243
)
242244

243245
# Assert auth request was successful
244-
self.assertEqual(status_code, 200)
246+
self.assertEqual(status_code, HTTPStatus.OK)
245247

246248
# Assert scope in response content
247249
self.assertEqual(response_scopes, sorted(APPLICATION_SCOPES_FULL))
@@ -252,7 +254,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
252254
# Assert access to userinfo end point?
253255
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_6.token)
254256
response = client.get('/v1/connect/userinfo')
255-
self.assertEqual(response.status_code, 200)
257+
self.assertEqual(response.status_code, HTTPStatus.OK)
256258

257259
# ------ TEST #7: Test token_3 from TEST #3 again. It should still have access, but no permission with status=403.
258260

@@ -262,13 +264,13 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
262264
# Test access to userinfo end point?
263265
response = client.get('/v1/connect/userinfo')
264266
content = json.loads(response.content)
265-
self.assertEqual(response.status_code, 403)
266-
self.assertEqual(content.get('detail', None), 'You do not have permission to perform this action.')
267+
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
268+
self.assertEqual(content.get('detail', None), 'Authentication credentials were not provided.')
267269

268270
# Verify token counts expected.
269-
self.assertEqual(AccessToken.objects.count(), 2)
270-
self.assertEqual(RefreshToken.objects.count(), 2)
271-
self.assertEqual(ArchivedToken.objects.count(), 2)
271+
self.assertEqual(AccessToken.objects.count(), 1)
272+
self.assertEqual(RefreshToken.objects.count(), 1)
273+
self.assertEqual(ArchivedToken.objects.count(), 3)
272274

273275
# Verify grant counts expected.
274276
self.assertEqual(DataAccessGrant.objects.count(), 1)
@@ -280,15 +282,15 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
280282

281283
# Perform partial authorization request, with out application getting an access token.
282284
response = self.client.post(reverse('oauth2_provider:authorize'), data=payload)
283-
self.assertEqual(response.status_code, 302)
285+
self.assertEqual(response.status_code, HTTPStatus.FOUND)
284286

285287
# Setup token_3 in APIClient from previous step. It should be removed now?
286288
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_3.token)
287289

288290
# Test access to userinfo end point?
289291
response = client.get('/v1/connect/userinfo')
290292
content = json.loads(response.content)
291-
self.assertEqual(response.status_code, 401)
293+
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
292294
self.assertEqual(content.get('detail', None), 'Authentication credentials were not provided.')
293295

294296
# Verify token counts expected.
@@ -309,7 +311,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
309311
)
310312

311313
# Assert auth request was successful
312-
self.assertEqual(status_code, 200)
314+
self.assertEqual(status_code, HTTPStatus.OK)
313315

314316
# Verify token counts expected.
315317
self.assertEqual(AccessToken.objects.count(), 1)
@@ -323,14 +325,14 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
323325
# Assert access to userinfo end point?
324326
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_9.token)
325327
response = client.get('/v1/connect/userinfo')
326-
self.assertEqual(response.status_code, 200)
328+
self.assertEqual(response.status_code, HTTPStatus.OK)
327329

328330
# Beneficiary chooses the DENY button choice on consent page
329331
payload['allow'] = False
330332

331333
# Perform partial authorization request, with out application getting an access token.
332334
response = self.client.post(reverse('oauth2_provider:authorize'), data=payload)
333-
self.assertEqual(response.status_code, 302)
335+
self.assertEqual(response.status_code, HTTPStatus.FOUND)
334336

335337
# Verify token counts expected.
336338
self.assertEqual(AccessToken.objects.count(), 1)
@@ -346,7 +348,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
346348
# when the allow parameter is false
347349
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_9.token)
348350
response = client.get('/v1/connect/userinfo')
349-
self.assertEqual(response.status_code, 200)
351+
self.assertEqual(response.status_code, HTTPStatus.OK)
350352

351353
# BB2-4270: Remove prior active tokens so tests below are not looking for multiple active tokens
352354
# which is an impossible state
@@ -360,12 +362,12 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
360362
payload['allow'] = True
361363

362364
# Perform authorization request
363-
token_10, refresh_token_10, status_code, response_scopes, access_token_scopes = self._authorize_and_request_token(
364-
payload, application
365+
token_10, refresh_token_10, status_code, response_scopes, access_token_scopes = (
366+
self._authorize_and_request_token(payload, application)
365367
)
366368

367369
# Assert auth request was successful
368-
self.assertEqual(status_code, 200)
370+
self.assertEqual(status_code, HTTPStatus.OK)
369371

370372
# Verify token counts expected.
371373
self.assertEqual(AccessToken.objects.count(), 1)
@@ -379,15 +381,15 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
379381
# Assert access to userinfo end point?
380382
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_10.token)
381383
response = client.get('/v1/connect/userinfo')
382-
self.assertEqual(response.status_code, 200)
384+
self.assertEqual(response.status_code, HTTPStatus.OK)
383385

384386
# Application changes choice to require demographic scopes
385387
application.require_demographic_scopes = False
386388
application.save()
387389

388390
# Perform partial authorization request, with out application getting an access token.
389391
response = self.client.post(reverse('oauth2_provider:authorize'), data=payload)
390-
self.assertEqual(response.status_code, 302)
392+
self.assertEqual(response.status_code, HTTPStatus.FOUND)
391393

392394
# Verify token counts expected.
393395
self.assertEqual(AccessToken.objects.count(), 0)
@@ -400,7 +402,7 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
400402

401403
# Perform partial authorization request, with out application getting an access token.
402404
response = self.client.post(reverse('oauth2_provider:authorize'), data=payload)
403-
self.assertEqual(response.status_code, 302)
405+
self.assertEqual(response.status_code, HTTPStatus.FOUND)
404406

405407
# Verify token counts expected.
406408
self.assertEqual(AccessToken.objects.count(), 0)
@@ -414,5 +416,5 @@ def test_bene_demo_scopes_change(self, mock_get_and_update):
414416
# Assert access to userinfo end point?
415417
client.credentials(HTTP_AUTHORIZATION='Bearer ' + token_10.token)
416418
response = client.get('/v1/connect/userinfo')
417-
self.assertEqual(response.status_code, 401)
419+
self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED)
418420
self.assertEqual(content.get('detail', None), 'Authentication credentials were not provided.')

apps/dot_ext/tests/test_utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1+
from unittest.mock import MagicMock, patch
2+
13
from django.test import TestCase
24

35
from apps.dot_ext.constants import SUPPORTED_VERSION_TEST_CASES
6+
<<<<<<< HEAD
47
from apps.dot_ext.utils import get_api_version_number_from_url, validate_latin_extended_string
8+
=======
9+
from apps.dot_ext.utils import (
10+
get_api_version_number_from_url,
11+
remove_application_user_pair_tokens_data_access,
12+
validate_latin_extended_string,
13+
)
14+
>>>>>>> 2e2d4da2 (BB2-4781: Revoke prior tokens on new auth flow (#1605))
515
from apps.versions import VersionNotMatched
616

717

@@ -33,3 +43,28 @@ def test_latin_extended_failure(self):
3343

3444
for text in invalid_inputs:
3545
assert not validate_latin_extended_string(text)
46+
47+
@patch('apps.dot_ext.utils.AccessToken')
48+
@patch('apps.dot_ext.utils.DataAccessGrant')
49+
@patch('apps.dot_ext.utils.RefreshToken')
50+
def test_remove_application_user_pair_tokens_data_access_delete_access_tokens_not_grant(
51+
self, mock_refresh_token, mock_data_access_grant, mock_access_token
52+
) -> None:
53+
application = MagicMock()
54+
user = MagicMock()
55+
access_token_queryset = MagicMock()
56+
mock_access_token.objects.filter.return_value = access_token_queryset
57+
data_access_grant_queryset = MagicMock()
58+
mock_data_access_grant.objects.filter.return_value = data_access_grant_queryset
59+
refresh_token_queryset = MagicMock()
60+
mock_refresh_token.objects.filter.return_value = refresh_token_queryset
61+
62+
remove_application_user_pair_tokens_data_access(application, user, False, True)
63+
64+
mock_access_token.objects.filter.assert_called_once_with(application=application, user=user)
65+
access_token_queryset.delete.assert_called_once()
66+
67+
data_access_grant_queryset.delete.assert_not_called()
68+
69+
mock_refresh_token.objects.filter.assert_called_once_with(application=application, user=user)
70+
refresh_token_queryset.delete.assert_called_once()

0 commit comments

Comments
 (0)