Skip to content

Commit c2243dc

Browse files
committed
Ensure the django user sets the oauthlib request user
1 parent cbc4b77 commit c2243dc

File tree

6 files changed

+58
-4
lines changed

6 files changed

+58
-4
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 Callable, Optional, Union
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[Union[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 set_oauthlib_user_to_device_request_user, user_code_generator
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,7 @@
33

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

78

89
@functools.lru_cache()
@@ -75,3 +76,25 @@ def user_code_generator(user_code_length: int = 8) -> str:
7576
user_code[i] = random.choice(character_space)
7677

7778
return "".join(user_code)
79+
80+
81+
def set_oauthlib_user_to_device_request_user(request: Request) -> None:
82+
"""
83+
The user isn't known when the device flow is initiated by a device.
84+
All we know is the client_id.
85+
86+
However, when the user logins in order to submit the user code
87+
from the device we now know which user is trying to authenticate
88+
their device. We update the device user field at this point
89+
and save it in the db.
90+
91+
This function is added to the pre_token stage during the device code grant's
92+
create_token_response where we have the oauthlib Request object which is what's used
93+
to populate the user field in the device model
94+
"""
95+
# Since this function is used in the settings module, it will lead to circular imports
96+
# since django isn't fully initialised yet when settings run
97+
from oauth2_provider.models import Device, get_device_model
98+
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import environ
1717

18-
from oauth2_provider.utils import user_code_generator
18+
from oauth2_provider.utils import set_oauthlib_user_to_device_request_user, user_code_generator
1919

2020

2121
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -202,7 +202,9 @@
202202
OAUTH2_PROVIDER = {
203203
"OAUTH2_VALIDATOR_CLASS": "idp.oauth.CustomOAuth2Validator",
204204
"OAUTH_DEVICE_VERIFICATION_URI": "http://127.0.0.1:8000/o/device",
205+
"OAUTH_PRE_TOKEN_VALIDATION": [set_oauthlib_user_to_device_request_user],
205206
"OAUTH_DEVICE_USER_CODE_GENERATOR": user_code_generator,
207+
"OAUTH_DEVICE_VERIFICATION_URI_COMPLETE": lambda x: f"http://127.0.0.1:8000/o/device?user_code={x}",
206208
"OIDC_ENABLED": env("OAUTH2_PROVIDER_OIDC_ENABLED"),
207209
"OIDC_RP_INITIATED_LOGOUT_ENABLED": env("OAUTH2_PROVIDER_OIDC_RP_INITIATED_LOGOUT_ENABLED"),
208210
# 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: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@
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 (
12+
get_access_token_model,
13+
get_application_model,
14+
get_device_model,
15+
get_refresh_token_model,
16+
)
17+
from oauth2_provider.utils import set_oauthlib_user_to_device_request_user
1218

1319
from . import presets
1420
from .common_testing import OAuth2ProviderTestCase as TestCase
1521

1622

1723
Application = get_application_model()
1824
AccessToken = get_access_token_model()
25+
RefreshToken = get_refresh_token_model()
1926
UserModel = get_user_model()
2027
DeviceModel: oauth2_provider.models.Device = get_device_model()
2128

@@ -122,6 +129,8 @@ def test_device_flow_authorization_user_code_confirm_and_access_token(self):
122129
# -----------------------
123130
self.oauth2_settings.OAUTH_DEVICE_VERIFICATION_URI = "example.com/device"
124131
self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz"
132+
self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz"
133+
self.oauth2_settings.OAUTH_PRE_TOKEN_VALIDATION = [set_oauthlib_user_to_device_request_user]
125134

126135
request_data: dict[str, str] = {
127136
"client_id": self.application.client_id,
@@ -193,6 +202,7 @@ def test_device_flow_authorization_user_code_confirm_and_access_token(self):
193202
"client_id": self.application.client_id,
194203
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
195204
}
205+
196206
token_response = self.client.post(
197207
"/o/token/",
198208
data=urlencode(token_payload),
@@ -207,6 +217,17 @@ def test_device_flow_authorization_user_code_confirm_and_access_token(self):
207217
assert token_data["token_type"].lower() == "bearer"
208218
assert "scope" in token_data
209219

220+
# ensure the access token and refresh token have the same user as the device that just authenticated
221+
access_token: oauth2_provider.models.AccessToken = AccessToken.objects.get(
222+
token=token_data["access_token"]
223+
)
224+
assert access_token.user == device.user
225+
226+
refresh_token: oauth2_provider.models.RefreshToken = RefreshToken.objects.get(
227+
token=token_data["refresh_token"]
228+
)
229+
assert refresh_token.user == device.user
230+
210231
@mock.patch(
211232
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
212233
lambda: "abc",

0 commit comments

Comments
 (0)