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:
Dionisio
2026-03-01 11:54:43 +01:00
parent 2421724b80
commit 48d853360e
18 changed files with 1789 additions and 2 deletions
+17
View File
@@ -163,10 +163,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if config.features.enable_auth {
use interfaces::api::handlers::auth_handler::auth_routes;
use oxicloud::interfaces::api::handlers::device_auth_handler;
use oxicloud::interfaces::middleware::auth::auth_middleware;
let auth_router = auth_routes().with_state(app_state.clone());
// Device Authorization Grant (RFC 8628)
// Public endpoints: /api/auth/device/authorize + /api/auth/device/token
let device_public = device_auth_handler::device_auth_public_routes()
.with_state(app_state.clone());
// Protected endpoints: /api/auth/device/verify, /api/auth/device/devices
let device_protected = device_auth_handler::device_auth_protected_routes()
.layer(axum::middleware::from_fn_with_state(
app_state.clone(),
auth_middleware,
))
.with_state(app_state.clone());
// Protected API routes — require valid JWT token
let protected_api = api_routes.layer(axum::middleware::from_fn_with_state(
app_state.clone(),
@@ -190,6 +203,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
app = Router::new()
// Auth endpoints (login, register, refresh) are public — no middleware
.nest("/api/auth", auth_router)
// Device Auth Grant public endpoints (authorize + token polling)
.nest("/api/auth/device", device_public)
// Device Auth Grant protected endpoints (verify + device management)
.nest("/api/auth/device", device_protected)
// Public API routes (share access, i18n) — no auth required
.nest("/api", public_api_routes)
// All other API routes are protected by auth middleware