48d853360e
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
100 lines
4.0 KiB
SQL
100 lines
4.0 KiB
SQL
-- ============================================================
|
|
-- Migration 003: OAuth 2.0 Device Authorization Grant (RFC 8628)
|
|
-- ============================================================
|
|
-- Adds the device_codes table to support the Device Authorization
|
|
-- Grant flow for WebDAV/CalDAV/CardDAV client authentication.
|
|
--
|
|
-- Flow:
|
|
-- 1. Client POSTs to /api/auth/device/authorize → receives device_code + user_code
|
|
-- 2. User opens verification_uri in browser, authenticates, enters user_code
|
|
-- 3. Client polls /api/auth/device/token with device_code
|
|
-- 4. Once approved, client receives access_token + refresh_token
|
|
-- ============================================================
|
|
|
|
-- Device code status enum
|
|
DO $BODY$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_type t
|
|
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
WHERE t.typname = 'device_code_status' AND n.nspname = 'auth'
|
|
) THEN
|
|
CREATE TYPE auth.device_code_status AS ENUM (
|
|
'pending', -- Waiting for user to authorize
|
|
'authorized', -- User approved, tokens ready for polling client
|
|
'denied', -- User denied the request
|
|
'expired' -- TTL exceeded without user action
|
|
);
|
|
END IF;
|
|
END $BODY$;
|
|
|
|
-- Device authorization codes table
|
|
CREATE TABLE IF NOT EXISTS auth.device_codes (
|
|
-- Unique row ID
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
|
|
-- RFC 8628 §3.2: device_code — long opaque token sent to the client for polling
|
|
device_code VARCHAR(128) UNIQUE NOT NULL,
|
|
|
|
-- RFC 8628 §3.2: user_code — short human-readable code shown on the client
|
|
-- and entered by the user on the verification page (e.g. "ABCD-1234")
|
|
user_code VARCHAR(16) UNIQUE NOT NULL,
|
|
|
|
-- Name/description of the client requesting access (shown to user)
|
|
client_name VARCHAR(255) NOT NULL DEFAULT 'Unknown Client',
|
|
|
|
-- Comma-separated scopes requested (e.g. "webdav,caldav,carddav")
|
|
scopes VARCHAR(512) NOT NULL DEFAULT 'webdav,caldav,carddav',
|
|
|
|
-- Current status of the device flow
|
|
status auth.device_code_status NOT NULL DEFAULT 'pending',
|
|
|
|
-- User who authorized the request (NULL until status = 'authorized')
|
|
user_id VARCHAR(36) REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
|
|
-- Tokens generated after authorization (NULL until status = 'authorized')
|
|
-- Stored encrypted/hashed depending on sensitivity
|
|
access_token TEXT,
|
|
refresh_token TEXT,
|
|
|
|
-- RFC 8628 §3.2: verification_uri — full URL the user must visit
|
|
verification_uri TEXT NOT NULL,
|
|
|
|
-- RFC 8628 §3.2: verification_uri_complete — URL with user_code pre-filled
|
|
verification_uri_complete TEXT,
|
|
|
|
-- RFC 8628 §3.2: expires_in — encoded as an absolute timestamp
|
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
|
|
-- RFC 8628 §3.2: interval — minimum polling interval in seconds
|
|
poll_interval_secs INTEGER NOT NULL DEFAULT 5,
|
|
|
|
-- Last time the client polled (for slow_down enforcement)
|
|
last_poll_at TIMESTAMP WITH TIME ZONE,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
authorized_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
-- Index for client polling by device_code (hot path)
|
|
CREATE INDEX IF NOT EXISTS idx_device_codes_device_code
|
|
ON auth.device_codes(device_code);
|
|
|
|
-- Index for user verification page lookup by user_code
|
|
CREATE INDEX IF NOT EXISTS idx_device_codes_user_code
|
|
ON auth.device_codes(user_code)
|
|
WHERE status = 'pending';
|
|
|
|
-- Index for cleanup of expired entries
|
|
CREATE INDEX IF NOT EXISTS idx_device_codes_expires_at
|
|
ON auth.device_codes(expires_at)
|
|
WHERE status = 'pending';
|
|
|
|
-- Index for user's authorized devices
|
|
CREATE INDEX IF NOT EXISTS idx_device_codes_user_id
|
|
ON auth.device_codes(user_id)
|
|
WHERE status = 'authorized';
|
|
|
|
COMMENT ON TABLE auth.device_codes IS 'OAuth 2.0 Device Authorization Grant (RFC 8628) codes for DAV client authentication';
|