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:
@@ -0,0 +1,96 @@
|
||||
//! DTOs for OAuth 2.0 Device Authorization Grant (RFC 8628).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================================
|
||||
// Request DTOs
|
||||
// ============================================================================
|
||||
|
||||
/// POST /api/auth/device/authorize — request body
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeviceAuthorizeRequestDto {
|
||||
/// Human-readable name of the client (e.g. "rclone", "DAVx⁵")
|
||||
#[serde(default = "default_client_name")]
|
||||
pub client_name: String,
|
||||
/// Comma-separated scopes (e.g. "webdav,caldav,carddav")
|
||||
#[serde(default = "default_scopes")]
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
fn default_client_name() -> String {
|
||||
"Unknown Client".to_string()
|
||||
}
|
||||
|
||||
fn default_scopes() -> String {
|
||||
"webdav,caldav,carddav".to_string()
|
||||
}
|
||||
|
||||
/// POST /api/auth/device/verify — user submits the code from the browser
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeviceVerifyRequestDto {
|
||||
/// The user_code displayed on the client device
|
||||
pub user_code: String,
|
||||
/// Whether the user approves ("approve") or denies ("deny")
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// POST /api/auth/device/token — client polls for tokens
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeviceTokenRequestDto {
|
||||
/// The device_code received from the initial authorize call
|
||||
pub device_code: String,
|
||||
/// Must be "urn:ietf:params:oauth:grant-type:device_code"
|
||||
#[serde(default)]
|
||||
pub grant_type: String,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response DTOs
|
||||
// ============================================================================
|
||||
|
||||
/// Response to POST /api/auth/device/authorize (RFC 8628 §3.2)
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeviceAuthorizeResponseDto {
|
||||
/// The device verification code
|
||||
pub device_code: String,
|
||||
/// The end-user verification code (short, human-readable)
|
||||
pub user_code: String,
|
||||
/// The end-user verification URI
|
||||
pub verification_uri: String,
|
||||
/// Optional: verification URI with user_code pre-filled
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub verification_uri_complete: Option<String>,
|
||||
/// Lifetime in seconds of the device_code and user_code
|
||||
pub expires_in: i64,
|
||||
/// Minimum polling interval in seconds
|
||||
pub interval: i32,
|
||||
}
|
||||
|
||||
/// Response to POST /api/auth/device/token when authorization is still pending
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeviceTokenPendingDto {
|
||||
pub error: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_description: Option<String>,
|
||||
}
|
||||
|
||||
/// Response to POST /api/auth/device/token when authorization is complete
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeviceTokenSuccessDto {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_in: i64,
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
/// GET /api/auth/device/verify — info about the pending device code
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DeviceVerifyInfoDto {
|
||||
/// The client name requesting access
|
||||
pub client_name: String,
|
||||
/// Scopes being requested
|
||||
pub scopes: String,
|
||||
/// Whether the user_code is valid and pending
|
||||
pub valid: bool,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod address_book_dto;
|
||||
pub mod calendar_dto;
|
||||
pub mod contact_dto;
|
||||
pub mod device_auth_dto;
|
||||
pub mod display_helpers;
|
||||
pub mod favorites_dto;
|
||||
pub mod file_dto;
|
||||
|
||||
Reference in New Issue
Block a user