|
3 | 3 | import time
|
4 | 4 | import uuid
|
5 | 5 | 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 |
7 | 9 | from urllib.parse import parse_qsl, urlparse
|
8 | 10 |
|
9 | 11 | from django.apps import apps
|
@@ -86,12 +88,14 @@ class AbstractApplication(models.Model):
|
86 | 88 | )
|
87 | 89 |
|
88 | 90 | GRANT_AUTHORIZATION_CODE = "authorization-code"
|
| 91 | + GRANT_DEVICE_CODE = "urn:ietf:params:oauth:grant-type:device_code" |
89 | 92 | GRANT_IMPLICIT = "implicit"
|
90 | 93 | GRANT_PASSWORD = "password"
|
91 | 94 | GRANT_CLIENT_CREDENTIALS = "client-credentials"
|
92 | 95 | GRANT_OPENID_HYBRID = "openid-hybrid"
|
93 | 96 | GRANT_TYPES = (
|
94 | 97 | (GRANT_AUTHORIZATION_CODE, _("Authorization code")),
|
| 98 | + (GRANT_DEVICE_CODE, _("Device Code")), |
95 | 99 | (GRANT_IMPLICIT, _("Implicit")),
|
96 | 100 | (GRANT_PASSWORD, _("Resource owner password-based")),
|
97 | 101 | (GRANT_CLIENT_CREDENTIALS, _("Client credentials")),
|
@@ -650,11 +654,93 @@ class Meta(AbstractIDToken.Meta):
|
650 | 654 | swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL"
|
651 | 655 |
|
652 | 656 |
|
| 657 | +class AbstractDevice(models.Model): |
| 658 | + class Meta: |
| 659 | + abstract = True |
| 660 | + constraints = [ |
| 661 | + models.UniqueConstraint( |
| 662 | + fields=["device_code"], |
| 663 | + name="unique_device_code", |
| 664 | + ), |
| 665 | + ] |
| 666 | + |
| 667 | + AUTHORIZED = "authorized" |
| 668 | + AUTHORIZATION_PENDING = "authorization-pending" |
| 669 | + EXPIRED = "expired" |
| 670 | + DENIED = "denied" |
| 671 | + |
| 672 | + DEVICE_FLOW_STATUS = ( |
| 673 | + (AUTHORIZED, _("Authorized")), |
| 674 | + (AUTHORIZATION_PENDING, _("Authorization pending")), |
| 675 | + (EXPIRED, _("Expired")), |
| 676 | + (DENIED, _("Denied")), |
| 677 | + ) |
| 678 | + |
| 679 | + id = models.BigAutoField(primary_key=True) |
| 680 | + device_code = models.CharField(max_length=100, unique=True) |
| 681 | + user_code = models.CharField(max_length=100) |
| 682 | + scope = models.CharField(max_length=64, default="openid") |
| 683 | + interval = models.IntegerField(default=5) |
| 684 | + expires = models.DateTimeField() |
| 685 | + status = models.CharField( |
| 686 | + max_length=64, blank=True, choices=DEVICE_FLOW_STATUS, default=AUTHORIZATION_PENDING |
| 687 | + ) |
| 688 | + client_id = models.CharField(max_length=100, default=generate_client_id, db_index=True) |
| 689 | + last_checked = models.DateTimeField(auto_now=True) |
| 690 | + |
| 691 | + |
| 692 | +class DeviceManager(models.Manager): |
| 693 | + def get_by_natural_key(self, client_id, device_code, user_code): |
| 694 | + return self.get(client_id=client_id, device_code=device_code, user_code=user_code) |
| 695 | + |
| 696 | + |
| 697 | +class Device(AbstractDevice): |
| 698 | + objects = DeviceManager() |
| 699 | + |
| 700 | + class Meta(AbstractDevice.Meta): |
| 701 | + swappable = "OAUTH2_PROVIDER_DEVICE_MODEL" |
| 702 | + |
| 703 | + def natural_key(self): |
| 704 | + return (self.client_id, self.device_code, self.user_code) |
| 705 | + |
| 706 | + |
| 707 | +@dataclass |
| 708 | +class DeviceRequest: |
| 709 | + client_id: str |
| 710 | + scope: str = "openid" |
| 711 | + |
| 712 | + |
| 713 | +@dataclass |
| 714 | +class DeviceCodeResponse: |
| 715 | + verification_uri: str |
| 716 | + expires_in: int |
| 717 | + user_code: int |
| 718 | + device_code: str |
| 719 | + interval: int |
| 720 | + |
| 721 | + |
| 722 | +def create_device(device_request: DeviceRequest, device_response: DeviceCodeResponse) -> Device: |
| 723 | + now = datetime.now(tz=dt_timezone.utc) |
| 724 | + |
| 725 | + return Device.objects.create( |
| 726 | + client_id=device_request.client_id, |
| 727 | + device_code=device_response.device_code, |
| 728 | + user_code=device_response.user_code, |
| 729 | + scope=device_request.scope, |
| 730 | + expires=now + timedelta(seconds=device_response.expires_in), |
| 731 | + ) |
| 732 | + |
| 733 | + |
653 | 734 | def get_application_model():
|
654 | 735 | """Return the Application model that is active in this project."""
|
655 | 736 | return apps.get_model(oauth2_settings.APPLICATION_MODEL)
|
656 | 737 |
|
657 | 738 |
|
| 739 | +def get_device_model(): |
| 740 | + """Return the Device model that is active in this project.""" |
| 741 | + return apps.get_model(oauth2_settings.DEVICE_MODEL) |
| 742 | + |
| 743 | + |
658 | 744 | def get_grant_model():
|
659 | 745 | """Return the Grant model that is active in this project."""
|
660 | 746 | return apps.get_model(oauth2_settings.GRANT_MODEL)
|
|
0 commit comments