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
+6
View File
@@ -24,6 +24,7 @@ pub fn create_web_routes() -> Router<Arc<AppState>> {
.route("/login", get(serve_login_page))
.route("/profile", get(serve_profile_page))
.route("/admin", get(serve_admin_page))
.route("/device", get(serve_device_verify_page))
// Serve static files with compression + cache headers
.fallback_service(static_service)
.layer(CompressionLayer::new().br(true).gzip(true))
@@ -47,3 +48,8 @@ async fn serve_profile_page() -> Html<&'static str> {
async fn serve_admin_page() -> Html<&'static str> {
Html(include_str!("../../../static/admin.html"))
}
/// Serve the device verification page (RFC 8628 Device Authorization Grant)
async fn serve_device_verify_page() -> Html<&'static str> {
Html(include_str!("../../../static/device-verify.html"))
}