Skip to content

Commit 0905607

Browse files
committed
Ensure the django user sets the oauthlib request user
1 parent c9c4635 commit 0905607

File tree

6 files changed

+51
-7
lines changed

6 files changed

+51
-7
lines changed

oauth2_provider/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass
77
from datetime import datetime, timedelta
88
from datetime import timezone as dt_timezone
9-
from typing import Optional
9+
from typing import Optional, Callable
1010
from urllib.parse import parse_qsl, urlparse
1111

1212
from django.apps import apps
@@ -734,6 +734,7 @@ class DeviceCodeResponse:
734734
user_code: int
735735
device_code: str
736736
interval: int
737+
verification_uri_complete: Optional[str| Callable] = None
737738

738739

739740
def create_device(device_request: DeviceRequest, device_response: DeviceCodeResponse) -> Device:

oauth2_provider/settings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from django.utils.module_loading import import_string
2525
from oauthlib.common import Request
2626

27-
from oauth2_provider.utils import user_code_generator
27+
from oauth2_provider.utils import user_code_generator, set_oauthlib_user_to_device_request_user
2828

2929

3030
USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None)
@@ -43,7 +43,9 @@
4343
"CLIENT_SECRET_HASHER": "default",
4444
"ACCESS_TOKEN_GENERATOR": None,
4545
"OAUTH_DEVICE_VERIFICATION_URI": None,
46+
"OAUTH_DEVICE_VERIFICATION_URI_COMPLETE": None,
4647
"OAUTH_DEVICE_USER_CODE_GENERATOR": user_code_generator,
48+
"OAUTH_PRE_TOKEN_VALIDATION": [set_oauthlib_user_to_device_request_user],
4749
"REFRESH_TOKEN_GENERATOR": None,
4850
"EXTRA_SERVER_KWARGS": {},
4951
"OAUTH2_SERVER_CLASS": "oauthlib.oauth2.Server",
@@ -276,8 +278,10 @@ def server_kwargs(self):
276278
("token_generator", "ACCESS_TOKEN_GENERATOR"),
277279
("refresh_token_generator", "REFRESH_TOKEN_GENERATOR"),
278280
("verification_uri", "OAUTH_DEVICE_VERIFICATION_URI"),
281+
("verification_uri_complete", "OAUTH_DEVICE_VERIFICATION_URI_COMPLETE"),
279282
("interval", "DEVICE_FLOW_INTERVAL"),
280283
("user_code_generator", "OAUTH_DEVICE_USER_CODE_GENERATOR"),
284+
("pre_token","OAUTH_PRE_TOKEN_VALIDATION")
281285
]
282286
}
283287
kwargs.update(self.EXTRA_SERVER_KWARGS)

oauth2_provider/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from django.conf import settings
55
from jwcrypto import jwk
6+
from oauthlib.common import Request
7+
68

79

810
@functools.lru_cache()
@@ -75,3 +77,24 @@ def user_code_generator(user_code_length: int = 8) -> str:
7577
user_code[i] = random.choice(character_space)
7678

7779
return "".join(user_code)
80+
81+
82+
def set_oauthlib_user_to_device_request_user(request: Request) -> None:
83+
"""
84+
The user isn't known when the device flow is initiated by a device.
85+
All we know is the client_id.
86+
87+
However, when the user logins in order to submit the user code
88+
from the device we now know which user is trying to authenticate
89+
their device. We update the device user field at this point
90+
and save it in the db.
91+
92+
This function is added to the pre_token stage during the device code grant's
93+
create_token_response where we have the oauthlib Request object which is what's used
94+
to populate the user field in the device model
95+
"""
96+
# Since this function is used in the settings module, it will lead to circular imports
97+
# since django isn't fully initialised yet when settings run
98+
from oauth2_provider.models import Device, get_device_model
99+
device: Device = get_device_model().objects.get(device_code=request._params["device_code"])
100+
request.user = device.user

oauth2_provider/views/device.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def device_user_code_view(request):
5959
user_code: str = form.cleaned_data["user_code"]
6060
device: Device = get_device_model().objects.get(user_code=user_code)
6161

62+
device.user = request.user
63+
device.save(update_fields=["user"])
64+
6265
if device is None:
6366
form.add_error("user_code", "Incorrect user code")
6467
return render(request, "oauth2_provider/device/user_code.html", {"form": form})

tests/app/idp/idp/settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515

1616
import environ
1717

18-
from oauth2_provider.utils import user_code_generator
19-
20-
18+
from oauth2_provider.utils import user_code_generator, set_oauthlib_user_to_device_request_user
2119
# Build paths inside the project like this: BASE_DIR / 'subdir'.
2220
BASE_DIR = Path(__file__).resolve().parent.parent
2321

@@ -202,7 +200,9 @@
202200
OAUTH2_PROVIDER = {
203201
"OAUTH2_VALIDATOR_CLASS": "idp.oauth.CustomOAuth2Validator",
204202
"OAUTH_DEVICE_VERIFICATION_URI": "http://127.0.0.1:8000/o/device",
203+
"OAUTH_PRE_TOKEN_VALIDATION": [set_oauthlib_user_to_device_request_user],
205204
"OAUTH_DEVICE_USER_CODE_GENERATOR": user_code_generator,
205+
"OAUTH_DEVICE_VERIFICATION_URI_COMPLETE": lambda x: f"http://127.0.0.1:8000/o/device?user_code={x}",
206206
"OIDC_ENABLED": env("OAUTH2_PROVIDER_OIDC_ENABLED"),
207207
"OIDC_RP_INITIATED_LOGOUT_ENABLED": env("OAUTH2_PROVIDER_OIDC_RP_INITIATED_LOGOUT_ENABLED"),
208208
# this key is just for out test app, you should never store a key like this in a production environment.

tests/test_device.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
from django.urls import reverse
99

1010
import oauth2_provider.models
11-
from oauth2_provider.models import get_access_token_model, get_application_model, get_device_model
11+
from oauth2_provider.models import get_access_token_model, get_application_model, get_device_model, get_refresh_token_model
12+
from oauth2_provider.utils import user_code_generator, set_oauthlib_user_to_device_request_user
1213

1314
from . import presets
1415
from .common_testing import OAuth2ProviderTestCase as TestCase
1516

1617

1718
Application = get_application_model()
1819
AccessToken = get_access_token_model()
20+
RefreshToken = get_refresh_token_model()
1921
UserModel = get_user_model()
2022
DeviceModel: oauth2_provider.models.Device = get_device_model()
2123

@@ -122,6 +124,8 @@ def test_device_flow_authorization_user_code_confirm_and_access_token(self):
122124
# -----------------------
123125
self.oauth2_settings.OAUTH_DEVICE_VERIFICATION_URI = "example.com/device"
124126
self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz"
127+
self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz"
128+
self.oauth2_settings.OAUTH_PRE_TOKEN_VALIDATION = [set_oauthlib_user_to_device_request_user]
125129

126130
request_data: dict[str, str] = {
127131
"client_id": self.application.client_id,
@@ -193,8 +197,9 @@ def test_device_flow_authorization_user_code_confirm_and_access_token(self):
193197
"client_id": self.application.client_id,
194198
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
195199
}
200+
196201
token_response = self.client.post(
197-
reverse("oauth2_provider:token"),
202+
"/o/token/",
198203
data=urlencode(token_payload),
199204
content_type="application/x-www-form-urlencoded",
200205
)
@@ -207,6 +212,14 @@ def test_device_flow_authorization_user_code_confirm_and_access_token(self):
207212
assert token_data["token_type"].lower() == "bearer"
208213
assert "scope" in token_data
209214

215+
# ensure the access token and refresh token have the same user as the device that just authenticated
216+
access_token: oauth2_provider.models.AccessToken = AccessToken.objects.get(token=token_data["access_token"])
217+
assert access_token.user == device.user
218+
219+
refresh_token: oauth2_provider.models.RefreshToken = RefreshToken.objects.get(token=token_data["refresh_token"])
220+
assert refresh_token.user == device.user
221+
222+
210223
@mock.patch(
211224
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
212225
lambda: "abc",

0 commit comments

Comments
 (0)