Skip to content

Commit f00c393

Browse files
shalehn2ygk
authored andcommitted
PR review response.
Document multiple database requires in advanced_topics.rst. Add an ImproperlyConfigured validator to the ready method of the DOTConfig app. Fix IDToken doc string. Document the use of _save_bearer_token. Define LocalIDToken and use it for validating the configuration test. Questionably, define py39-multi-db-invalid-token-configuration-dj42. This will consistently cause tox runs to fail until it is worked out how to mark this as an expected failure.
1 parent d367086 commit f00c393

File tree

9 files changed

+114
-20
lines changed

9 files changed

+114
-20
lines changed

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

oauth2_provider/apps.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
1-
from django.apps import AppConfig
1+
from django.apps import AppConfig, apps
2+
from django.core.exceptions import ImproperlyConfigured
3+
from django.db import router
24

35

46
class DOTConfig(AppConfig):
57
name = "oauth2_provider"
68
verbose_name = "Django OAuth Toolkit"
9+
10+
def _validate_token_configuration(self):
11+
from .settings import oauth2_settings
12+
13+
databases = set(
14+
router.db_for_write(apps.get_model(model))
15+
for model in (
16+
oauth2_settings.ACCESS_TOKEN_MODEL,
17+
oauth2_settings.ID_TOKEN_MODEL,
18+
oauth2_settings.REFRESH_TOKEN_MODEL,
19+
)
20+
)
21+
22+
# This is highly unlikely, but let's warn people just in case it does.
23+
# If the tokens were allowed to be in different databases this would require all
24+
# writes to have a transaction around each database. Instead, let's enforce that
25+
# they all live together in one database.
26+
# The tokens are not required to live in the default database provided the Django
27+
# routers know the correct database for them.
28+
if len(databases) > 1:
29+
raise ImproperlyConfigured("the token models are expected to be stored in the same database.")
30+
31+
def ready(self):
32+
self._validate_token_configuration()

oauth2_provider/models.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -501,17 +501,8 @@ def revoke(self):
501501
Mark this refresh token revoked and revoke related access token
502502
"""
503503
access_token_model = get_access_token_model()
504-
refresh_token_model = get_refresh_token_model()
505-
506504
access_token_database = router.db_for_write(access_token_model)
507-
refresh_token_database = router.db_for_write(refresh_token_model)
508-
509-
# This is highly unlikely, but let's warn people just in case it does.
510-
if access_token_database != refresh_token_database:
511-
logger.warning(
512-
"access token and refresh token are in separate databases but a transaction"
513-
" is only used for the access token"
514-
)
505+
refresh_token_model = get_refresh_token_model()
515506

516507
# Use the access_token_database instead of making the assumption it is in 'default'.
517508
with transaction.atomic(using=access_token_database):
@@ -655,7 +646,7 @@ def get_access_token_model():
655646

656647

657648
def get_id_token_model():
658-
"""Return the AccessToken model that is active in this project."""
649+
"""Return the IDToken model that is active in this project."""
659650
return apps.get_model(oauth2_settings.ID_TOKEN_MODEL)
660651

661652

oauth2_provider/oauth2_validators.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -558,14 +558,22 @@ def rotate_refresh_token(self, request):
558558
return oauth2_settings.ROTATE_REFRESH_TOKEN
559559

560560
def save_bearer_token(self, token, request, *args, **kwargs):
561+
"""
562+
Save access and refresh token.
563+
564+
Override _save_bearer_token and not this function when adding custom logic
565+
for the storing of these token. This allows the transaction logic to be
566+
separate from the token handling.
567+
"""
561568
# Use the AccessToken's database instead of making the assumption it is in 'default'.
562569
with transaction.atomic(using=router.db_for_write(AccessToken)):
563-
return self._save_bearer_token_internals(token, request, *args, **kwargs)
570+
return self._save_bearer_token(token, request, *args, **kwargs)
564571

565-
def _save_bearer_token_internals(self, token, request, *args, **kwargs):
572+
def _save_bearer_token(self, token, request, *args, **kwargs):
566573
"""
567-
Save access and refresh token, If refresh token is issued, remove or
568-
reuse old refresh token as in rfc:`6`
574+
Save access and refresh token.
575+
576+
If refresh token is issued, remove or reuse old refresh token as in rfc:`6`.
569577
570578
@see: https://rfc-editor.org/rfc/rfc6749.html#section-6
571579
"""

tests/db_router.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
apps_in_beta = {"some_other_app", "this_one_too"}
1+
apps_in_beta = {"tests", "some_other_app", "this_one_too"}
22

33
# These are bare minimum routers to fake the scenario where there is actually a
44
# decision around where an application's models might live.
5-
# alpha is where the core Django models are stored including user. To keep things
6-
# simple this is where the oauth2 provider models are stored as well because they
7-
# have a foreign key to User.
85

96

107
class AlphaRouter:
8+
# alpha is where the core Django models are stored including user. To keep things
9+
# simple this is where the oauth2 provider models are stored as well because they
10+
# have a foreign key to User.
11+
1112
def db_for_read(self, model, **hints):
1213
if model._meta.app_label not in apps_in_beta:
1314
return "alpha"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 3.2.25 on 2024-08-08 22:47
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import uuid
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
('tests', '0005_basetestapplication_allowed_origins_and_more'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='LocalIDToken',
20+
fields=[
21+
('id', models.BigAutoField(primary_key=True, serialize=False)),
22+
('jti', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='JWT Token ID')),
23+
('expires', models.DateTimeField()),
24+
('scope', models.TextField(blank=True)),
25+
('created', models.DateTimeField(auto_now_add=True)),
26+
('updated', models.DateTimeField(auto_now=True)),
27+
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
28+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tests_localidtoken', to=settings.AUTH_USER_MODEL)),
29+
],
30+
options={
31+
'abstract': False,
32+
},
33+
),
34+
]

tests/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
AbstractAccessToken,
55
AbstractApplication,
66
AbstractGrant,
7+
AbstractIDToken,
78
AbstractRefreshToken,
89
)
910
from oauth2_provider.settings import oauth2_settings
@@ -54,3 +55,9 @@ class SampleRefreshToken(AbstractRefreshToken):
5455

5556
class SampleGrant(AbstractGrant):
5657
custom_field = models.CharField(max_length=255)
58+
59+
60+
class LocalIDToken(AbstractIDToken):
61+
"""Exists to be improperly configured for multiple databases."""
62+
63+
# The other token types will be in 'alpha' database.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .multi_db_settings import * # noqa: F401, F403
2+
3+
4+
OAUTH2_PROVIDER = {
5+
# The other two tokens will be in alpha. This will cause a failure when the
6+
# app's ready method is called.
7+
"ID_TOKEN_MODEL": "tests.LocalIDToken",
8+
}

tox.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ envlist =
1212
py{310,311,312}-dj50,
1313
py{310,311,312}-djmain,
1414
py39-multi-db-dj-42,
15+
py39-multi-db-invalid-token-configuration-dj42,
1516

1617
[gh-actions]
1718
python =
@@ -122,6 +123,13 @@ setenv =
122123
PYTHONPATH = {toxinidir}
123124
PYTHONWARNINGS = all
124125

126+
[testenv:py39-multi-db-invalid-token-configuration-dj42]
127+
setenv =
128+
DJANGO_SETTINGS_MODULE = tests.multi_db_settings_invalid_token_configuration
129+
PYTHONPATH = {toxinidir}
130+
PYTHONWARNINGS = all
131+
ignore_errors = true
132+
125133
[testenv:migrate_swapped]
126134
setenv =
127135
DJANGO_SETTINGS_MODULE = tests.settings_swapped

0 commit comments

Comments
 (0)