Skip to content

Commit bbd4c6b

Browse files
authored
Merge branch 'master' into discussions
2 parents 9ab9252 + 9561866 commit bbd4c6b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+399
-86
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Rodney Richardson
102102
Rustem Saiargaliev
103103
Rustem Saiargaliev
104104
Sandro Rodrigues
105+
Sean 'Shaleh' Perry
105106
Shaheed Haque
106107
Shaun Stanworth
107108
Sayyid Hamid Mahdavi

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
* Update token to TextField from CharField with 255 character limit and SHA-256 checksum in AbstractAccessToken model. Removing the 255 character limit enables supporting JWT tokens with additional claims
2424
* Update middleware, validators, and views to use token checksums instead of token for token retrieval and validation.
2525
* #1446 use generic models pk instead of id.
26-
* Bump oauthlib version to 3.2.0 and above
26+
* Transactions wrapping writes of the Tokens now rely on Django's database routers to determine the correct
27+
database to use instead of assuming that 'default' is the correct one.
28+
* Bump oauthlib version to 3.2.2 and above
2729
* Update the OAuth2Validator's invalidate_authorization_code method to return an InvalidGrantError if the associated grant does not exist.
2830

2931
### Deprecated

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Requirements
4545

4646
* Python 3.8+
4747
* Django 4.2, 5.0 or 5.1
48-
* oauthlib 3.2+
48+
* oauthlib 3.2.2+
4949

5050
Installation
5151
------------

docs/advanced_topics.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ That's all, now Django OAuth Toolkit will use your model wherever an Application
6565
is because of the way Django currently implements swappable models.
6666
See `issue #90 <https://github.com/jazzband/django-oauth-toolkit/issues/90>`_ for details.
6767

68+
Configuring multiple databases
69+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
70+
71+
There is no requirement that the tokens are stored in the default database or that there is a
72+
default database provided the database routers can determine the correct Token locations. Because the
73+
Tokens have foreign keys to the ``User`` model, you likely want to keep the tokens in the same database
74+
as your User model. It is also important that all of the tokens are stored in the same database.
75+
This could happen for instance if one of the Tokens is locally overridden and stored in a separate database.
76+
The reason for this is transactions will only be made for the database where AccessToken is stored
77+
even when writing to RefreshToken or other tokens.
78+
6879
Multiple Grants
6980
~~~~~~~~~~~~~~~
7081

docs/contributing.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,26 @@ Open :file:`mycoverage/index.html` in your browser and you can see a coverage su
252252

253253
There's no need to wait for Codecov to complain after you submit your PR.
254254

255+
The tests are generic and written to work with both single database and multiple database configurations. tox will run
256+
tests both ways. You can see the configurations used in tests/settings.py and tests/multi_db_settings.py.
257+
258+
When there are multiple databases defined, Django tests will not work unless they are told which database(s) to work with.
259+
For test writers this means any test must either:
260+
- instead of Django's TestCase or TransactionTestCase use the versions of those
261+
classes defined in tests/common_testing.py
262+
- when using pytest's `django_db` mark, define it like this:
263+
`@pytest.mark.django_db(databases=retrieve_current_databases())`
264+
265+
In test code, anywhere the database is referenced the Django router needs to be used exactly like the package's code.
266+
267+
.. code-block:: python
268+
269+
token_database = router.db_for_write(AccessToken)
270+
with self.assertNumQueries(1, using=token_database):
271+
# call something using the database
272+
273+
Without the 'using' option, this test fails in the multiple database scenario because 'default' will be used instead.
274+
255275
Code conventions matter
256276
-----------------------
257277

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Requirements
2323

2424
* Python 3.8+
2525
* Django 4.2, 5.0 or 5.1
26-
* oauthlib 3.2+
26+
* oauthlib 3.2.2+
2727

2828
Index
2929
=====

docs/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Django
2-
oauthlib>=3.2.0
2+
oauthlib>=3.2.2
33
m2r>=0.2.1
44
mistune<2
55
sphinx==7.2.6

oauth2_provider/apps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@
44
class DOTConfig(AppConfig):
55
name = "oauth2_provider"
66
verbose_name = "Django OAuth Toolkit"
7+
8+
def ready(self):
9+
# Import checks to ensure they run.
10+
from . import checks # noqa: F401

oauth2_provider/checks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from django.apps import apps
2+
from django.core import checks
3+
from django.db import router
4+
5+
from .settings import oauth2_settings
6+
7+
8+
@checks.register(checks.Tags.database)
9+
def validate_token_configuration(app_configs, **kwargs):
10+
databases = set(
11+
router.db_for_write(apps.get_model(model))
12+
for model in (
13+
oauth2_settings.ACCESS_TOKEN_MODEL,
14+
oauth2_settings.ID_TOKEN_MODEL,
15+
oauth2_settings.REFRESH_TOKEN_MODEL,
16+
)
17+
)
18+
19+
# This is highly unlikely, but let's warn people just in case it does.
20+
# If the tokens were allowed to be in different databases this would require all
21+
# writes to have a transaction around each database. Instead, let's enforce that
22+
# they all live together in one database.
23+
# The tokens are not required to live in the default database provided the Django
24+
# routers know the correct database for them.
25+
if len(databases) > 1:
26+
return [checks.Error("The token models are expected to be stored in the same database.")]
27+
28+
return []

oauth2_provider/models.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
import logging
33
import time
44
import uuid
5+
from contextlib import suppress
56
from datetime import timedelta
67
from urllib.parse import parse_qsl, urlparse
78

89
from django.apps import apps
910
from django.conf import settings
1011
from django.contrib.auth.hashers import identify_hasher, make_password
1112
from django.core.exceptions import ImproperlyConfigured
12-
from django.db import models, transaction
13+
from django.db import models, router, transaction
1314
from django.urls import reverse
1415
from django.utils import timezone
1516
from django.utils.translation import gettext_lazy as _
@@ -512,17 +513,19 @@ def revoke(self):
512513
Mark this refresh token revoked and revoke related access token
513514
"""
514515
access_token_model = get_access_token_model()
516+
access_token_database = router.db_for_write(access_token_model)
515517
refresh_token_model = get_refresh_token_model()
516-
with transaction.atomic():
518+
519+
# Use the access_token_database instead of making the assumption it is in 'default'.
520+
with transaction.atomic(using=access_token_database):
517521
token = refresh_token_model.objects.select_for_update().filter(pk=self.pk, revoked__isnull=True)
518522
if not token:
519523
return
520524
self = list(token)[0]
521525

522-
try:
523-
access_token_model.objects.get(pk=self.access_token_id).revoke()
524-
except access_token_model.DoesNotExist:
525-
pass
526+
with suppress(access_token_model.DoesNotExist):
527+
access_token_model.objects.get(id=self.access_token_id).revoke()
528+
526529
self.access_token = None
527530
self.revoked = timezone.now()
528531
self.save()
@@ -655,7 +658,7 @@ def get_access_token_model():
655658

656659

657660
def get_id_token_model():
658-
"""Return the AccessToken model that is active in this project."""
661+
"""Return the IDToken model that is active in this project."""
659662
return apps.get_model(oauth2_settings.ID_TOKEN_MODEL)
660663

661664

0 commit comments

Comments
 (0)