feat: implement OAuth 2.0 Device Authorization Grant (RFC 8628) for WebDAV/CalDAV/CardDAV
Adds full Device Authorization Grant flow so DAV clients (rclone, etc.) can authenticate without browser-based OAuth redirects. New files: - Domain entity: DeviceCode with status lifecycle (pending/authorized/denied/expired) - Port: DeviceCodeStoragePort trait (7 async methods) - DTOs: request/response types for all device auth endpoints - Repository: DeviceCodePgRepository (PostgreSQL implementation) - Service: DeviceAuthService (initiate, verify, approve, deny, poll, cleanup) - Handler: 6 HTTP endpoints (2 public + 4 protected) - Static: device-verify.html verification page served at /device Flow: 1. Client POST /api/auth/device/authorize → device_code + user_code 2. User opens /device?code=XXXX in browser, approves 3. Client polls POST /api/auth/device/token → receives JWT tokens 4. Client uses Bearer token with existing WebDAV/CalDAV/CardDAV middleware Schema: auth.device_codes table + device_code_status enum added to schema.sql Closes #152
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::device_code::DeviceCode;
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::domain::entities::user::User;
|
||||
use async_trait::async_trait;
|
||||
@@ -202,3 +203,31 @@ pub trait SessionStoragePort: Send + Sync + 'static {
|
||||
/// Revokes all sessions of a user
|
||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Device Authorization Grant Port (RFC 8628)
|
||||
// ============================================================================
|
||||
|
||||
#[async_trait]
|
||||
pub trait DeviceCodeStoragePort: Send + Sync + 'static {
|
||||
/// Persist a new device code flow
|
||||
async fn create_device_code(&self, device_code: DeviceCode) -> Result<DeviceCode, DomainError>;
|
||||
|
||||
/// Find a device code by its opaque device_code token (used by client polling)
|
||||
async fn get_by_device_code(&self, device_code: &str) -> Result<DeviceCode, DomainError>;
|
||||
|
||||
/// Find a pending device code by the short user_code (used on verification page)
|
||||
async fn get_pending_by_user_code(&self, user_code: &str) -> Result<DeviceCode, DomainError>;
|
||||
|
||||
/// Update a device code (status change, token storage, poll timestamp, etc.)
|
||||
async fn update_device_code(&self, device_code: DeviceCode) -> Result<(), DomainError>;
|
||||
|
||||
/// Delete expired device codes (cleanup job)
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError>;
|
||||
|
||||
/// List authorized device codes for a user (for UI management)
|
||||
async fn list_by_user(&self, user_id: &str) -> Result<Vec<DeviceCode>, DomainError>;
|
||||
|
||||
/// Delete a specific device code by ID (revocation)
|
||||
async fn delete_by_id(&self, id: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user