Skip to content

Commit fe76b5e

Browse files
duzumakidopry
authored andcommitted
Add Device model
This model represents the device session for the request and response stage See section 3.1(https://datatracker.ietf.org/doc/html/rfc8628#section-3.1) and 3.2(https://datatracker.ietf.org/doc/html/rfc8628#section-3.2)
1 parent 362fecb commit fe76b5e

File tree

1 file changed

+104
-1
lines changed

1 file changed

+104
-1
lines changed

oauth2_provider/models.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import time
44
import uuid
55
from contextlib import suppress
6-
from datetime import timedelta
6+
from dataclasses import dataclass
7+
from datetime import datetime, timedelta
8+
from datetime import timezone as dt_timezone
9+
from typing import Optional
710
from urllib.parse import parse_qsl, urlparse
811

912
from django.apps import apps
@@ -86,12 +89,14 @@ class AbstractApplication(models.Model):
8689
)
8790

8891
GRANT_AUTHORIZATION_CODE = "authorization-code"
92+
GRANT_DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code"
8993
GRANT_IMPLICIT = "implicit"
9094
GRANT_PASSWORD = "password"
9195
GRANT_CLIENT_CREDENTIALS = "client-credentials"
9296
GRANT_OPENID_HYBRID = "openid-hybrid"
9397
GRANT_TYPES = (
9498
(GRANT_AUTHORIZATION_CODE, _("Authorization code")),
99+
(GRANT_DEVICE_CODE, _("Device Code")),
95100
(GRANT_IMPLICIT, _("Implicit")),
96101
(GRANT_PASSWORD, _("Resource owner password-based")),
97102
(GRANT_CLIENT_CREDENTIALS, _("Client credentials")),
@@ -650,11 +655,109 @@ class Meta(AbstractIDToken.Meta):
650655
swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL"
651656

652657

658+
class AbstractDevice(models.Model):
659+
class Meta:
660+
abstract = True
661+
constraints = [
662+
models.UniqueConstraint(
663+
fields=["device_code"],
664+
name="%(app_label)s_%(class)s_unique_device_code",
665+
),
666+
]
667+
668+
AUTHORIZED = "authorized"
669+
AUTHORIZATION_PENDING = "authorization-pending"
670+
EXPIRED = "expired"
671+
DENIED = "denied"
672+
673+
DEVICE_FLOW_STATUS = (
674+
(AUTHORIZED, _("Authorized")),
675+
(AUTHORIZATION_PENDING, _("Authorization pending")),
676+
(EXPIRED, _("Expired")),
677+
(DENIED, _("Denied")),
678+
)
679+
680+
id = models.BigAutoField(primary_key=True)
681+
user = models.ForeignKey(
682+
settings.AUTH_USER_MODEL,
683+
related_name="%(app_label)s_%(class)s",
684+
null=True,
685+
blank=True,
686+
on_delete=models.CASCADE,
687+
)
688+
device_code = models.CharField(max_length=100, unique=True)
689+
user_code = models.CharField(max_length=100)
690+
scope = models.CharField(max_length=64, null=True)
691+
interval = models.IntegerField(default=5)
692+
expires = models.DateTimeField()
693+
status = models.CharField(
694+
max_length=64, blank=True, choices=DEVICE_FLOW_STATUS, default=AUTHORIZATION_PENDING
695+
)
696+
client_id = models.CharField(max_length=100, db_index=True)
697+
last_checked = models.DateTimeField(auto_now=True)
698+
699+
def is_expired(self):
700+
"""
701+
Check device flow session expiration.
702+
"""
703+
now = datetime.now(tz=dt_timezone.utc)
704+
return now >= self.expires
705+
706+
707+
class DeviceManager(models.Manager):
708+
def get_by_natural_key(self, client_id, device_code, user_code):
709+
return self.get(client_id=client_id, device_code=device_code, user_code=user_code)
710+
711+
712+
class Device(AbstractDevice):
713+
objects = DeviceManager()
714+
715+
class Meta(AbstractDevice.Meta):
716+
swappable = "OAUTH2_PROVIDER_DEVICE_MODEL"
717+
718+
def natural_key(self):
719+
return (self.client_id, self.device_code, self.user_code)
720+
721+
722+
@dataclass
723+
class DeviceRequest:
724+
# https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
725+
# scope is optional
726+
client_id: str
727+
scope: Optional[str] = None
728+
729+
730+
@dataclass
731+
class DeviceCodeResponse:
732+
verification_uri: str
733+
expires_in: int
734+
user_code: int
735+
device_code: str
736+
interval: int
737+
738+
739+
def create_device(device_request: DeviceRequest, device_response: DeviceCodeResponse) -> Device:
740+
now = datetime.now(tz=dt_timezone.utc)
741+
742+
return Device.objects.create(
743+
client_id=device_request.client_id,
744+
device_code=device_response.device_code,
745+
user_code=device_response.user_code,
746+
scope=device_request.scope,
747+
expires=now + timedelta(seconds=device_response.expires_in),
748+
)
749+
750+
653751
def get_application_model():
654752
"""Return the Application model that is active in this project."""
655753
return apps.get_model(oauth2_settings.APPLICATION_MODEL)
656754

657755

756+
def get_device_model():
757+
"""Return the Device model that is active in this project."""
758+
return apps.get_model(oauth2_settings.DEVICE_MODEL)
759+
760+
658761
def get_grant_model():
659762
"""Return the Grant model that is active in this project."""
660763
return apps.get_model(oauth2_settings.GRANT_MODEL)

0 commit comments

Comments
 (0)