Skip to content

Commit

Permalink
avoid race condition on code exchange for token (fixes #410)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpaniagualaconich authored and juanifioren committed Sep 23, 2023
1 parent 3203ce7 commit 7d186ee
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 4 deletions.
6 changes: 5 additions & 1 deletion oidc_provider/lib/endpoints/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from base64 import urlsafe_b64encode

from django.contrib.auth import authenticate
from django.db import DatabaseError
from django.http import JsonResponse

from oidc_provider import settings
Expand Down Expand Up @@ -70,7 +71,10 @@ def validate_params(self):
raise TokenError('invalid_client')

try:
self.code = Code.objects.get(code=self.params['code'])
self.code = Code.objects.select_for_update(nowait=True).get(code=self.params['code'])
except DatabaseError:
logger.debug('[Token] Code cannot be reused: %s', self.params['code'])
raise TokenError('invalid_grant')
except Code.DoesNotExist:
logger.debug('[Token] Code does not exist: %s', self.params['code'])
raise TokenError('invalid_grant')
Expand Down
20 changes: 20 additions & 0 deletions oidc_provider/tests/cases/test_token_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from base64 import b64encode

from django.db import DatabaseError

try:
from urllib.parse import urlencode
except ImportError:
Expand Down Expand Up @@ -312,6 +314,24 @@ def test_authorization_code(self):
self.assertEqual(id_token['sub'], str(self.user.id))
self.assertEqual(id_token['aud'], self.client.client_id)

@override_settings(OIDC_TOKEN_EXPIRE=720)
def test_authorization_code_cant_be_reused(self):
"""
Authorization codes MUST be short lived and single-use,
as described in Section 10.5 of OAuth 2.0 [RFC6749].
"""
code = self._create_code()
post_data = self._auth_code_post_data(code=code.code)

with patch('django.db.models.query.QuerySet.select_for_update') as select_for_update_func:
select_for_update_func.side_effect = DatabaseError()
response = self._post_request(post_data)
select_for_update_func.assert_called_once()

self.assertEqual(response.status_code, 400)
response_dic = json.loads(response.content.decode('utf-8'))
self.assertEqual(response_dic['error'], 'invalid_grant')

@override_settings(OIDC_TOKEN_EXPIRE=720,
OIDC_IDTOKEN_INCLUDE_CLAIMS=True)
def test_scope_is_ignored_for_auth_code(self):
Expand Down
7 changes: 4 additions & 3 deletions oidc_provider/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from django.db import transaction
from django.contrib.auth import logout as django_user_logout
from django.http import JsonResponse, HttpResponse
from django.shortcuts import render
Expand Down Expand Up @@ -204,9 +205,9 @@ def post(self, request, *args, **kwargs):
token = self.token_endpoint_class(request)

try:
token.validate_params()

dic = token.create_response_dic()
with transaction.atomic():
token.validate_params()
dic = token.create_response_dic()

return self.token_endpoint_class.response(dic)

Expand Down

0 comments on commit 7d186ee

Please sign in to comment.