Device code for OAuth 2.0 Device Flow. This stores the device codes issued during the device authorization flow, along with their status and associated user information once authorized.
| 18 | |
| 19 | |
| 20 | class DeviceCode(Base): |
| 21 | """Device code for OAuth 2.0 Device Flow. |
| 22 | |
| 23 | This stores the device codes issued during the device authorization flow, |
| 24 | along with their status and associated user information once authorized. |
| 25 | """ |
| 26 | |
| 27 | __tablename__ = 'device_codes' |
| 28 | |
| 29 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) |
| 30 | device_code: Mapped[str] = mapped_column( |
| 31 | String(128), unique=True, nullable=False, index=True |
| 32 | ) |
| 33 | user_code: Mapped[str] = mapped_column( |
| 34 | String(16), unique=True, nullable=False, index=True |
| 35 | ) |
| 36 | status: Mapped[str] = mapped_column( |
| 37 | String(32), nullable=False, default=DeviceCodeStatus.PENDING.value |
| 38 | ) |
| 39 | |
| 40 | # Keycloak user ID who authorized the device (set during verification) |
| 41 | keycloak_user_id: Mapped[str | None] = mapped_column(String(255), nullable=True) |
| 42 | |
| 43 | # Timestamps |
| 44 | expires_at: Mapped[datetime] = mapped_column( |
| 45 | DateTime(timezone=True), nullable=False |
| 46 | ) |
| 47 | authorized_at: Mapped[datetime | None] = mapped_column( |
| 48 | DateTime(timezone=True), nullable=True |
| 49 | ) |
| 50 | |
| 51 | # Rate limiting fields for RFC 8628 section 3.5 compliance |
| 52 | last_poll_time: Mapped[datetime | None] = mapped_column( |
| 53 | DateTime(timezone=True), nullable=True |
| 54 | ) |
| 55 | current_interval: Mapped[int] = mapped_column(nullable=False, default=5) |
| 56 | |
| 57 | def __repr__(self) -> str: |
| 58 | return f"<DeviceCode(user_code='{self.user_code}', status='{self.status}')>" |
| 59 | |
| 60 | def is_expired(self) -> bool: |
| 61 | """Check if the device code has expired.""" |
| 62 | now = datetime.now(timezone.utc) |
| 63 | # Handle timezone-naive datetime from database by assuming it's UTC |
| 64 | expires_at = self.expires_at |
| 65 | if expires_at.tzinfo is None: |
| 66 | expires_at = expires_at.replace(tzinfo=timezone.utc) |
| 67 | return now > expires_at |
| 68 | |
| 69 | def is_pending(self) -> bool: |
| 70 | """Check if the device code is still pending authorization.""" |
| 71 | return self.status == DeviceCodeStatus.PENDING.value and not self.is_expired() |
| 72 | |
| 73 | def is_authorized(self) -> bool: |
| 74 | """Check if the device code has been authorized.""" |
| 75 | return self.status == DeviceCodeStatus.AUTHORIZED.value |
| 76 | |
| 77 | def authorize(self, user_id: str) -> None: |
no outgoing calls