fix: replace JWT tokens in URL query params with short-lived ephemeral tokens

Addresses CWE-598: long-lived JWTs were exposed in WebSocket URLs, file
download links, and Immich asset proxy URLs, leaking into server logs,
browser history, and Referer headers.

- Add ephemeralTokens service: in-memory single-use tokens with per-purpose
  TTLs (ws=30s, download/immich=60s), max 10k entries, periodic cleanup
- Add POST /api/auth/ws-token and POST /api/auth/resource-token endpoints
- WebSocket auth now consumes an ephemeral token instead of verifying the JWT
  directly from the URL; client fetches a fresh token before each connect
- File download ?token= query param now accepts ephemeral tokens; Bearer
  header path continues to accept JWTs for programmatic access
- Immich asset proxy replaces authFromQuery JWT injection with ephemeral token
  consumption
- Client: new getAuthUrl() utility, AuthedImg/ImmichImg components, and async
  onClick handlers replace the synchronous authUrl() pattern throughout
  FileManager, PlaceInspector, and MemoriesPanel
- Add OIDC_DISCOVERY_URL env var and oidc_discovery_url DB setting to allow
  overriding the auto-constructed discovery endpoint (required for Authentik
  and similar providers); exposed in the admin UI and .env.example
This commit is contained in:
jubnl
2026-04-01 05:42:27 +02:00
parent 0ee53e7b38
commit 78695b4e03
15 changed files with 267 additions and 87 deletions

View File

@@ -20,6 +20,7 @@ import { AuthRequest, OptionalAuthRequest, User } from '../types';
import { writeAudit, getClientIp } from '../services/auditLog';
import { decrypt_api_key, maybe_encrypt_api_key, encrypt_api_key } from '../services/apiKeyCrypto';
import { startTripReminders } from '../scheduler';
import { createEphemeralToken } from '../services/ephemeralTokens';
authenticator.options = { window: 1 };
@@ -951,4 +952,24 @@ router.delete('/mcp-tokens/:id', authenticate, (req: Request, res: Response) =>
res.json({ success: true });
});
// Short-lived single-use token for WebSocket connections (avoids JWT in WS URL)
router.post('/ws-token', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const token = createEphemeralToken(authReq.user.id, 'ws');
if (!token) return res.status(503).json({ error: 'Service unavailable' });
res.json({ token });
});
// Short-lived single-use token for direct resource URLs (file downloads, Immich assets)
router.post('/resource-token', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const { purpose } = req.body as { purpose?: string };
if (purpose !== 'download' && purpose !== 'immich') {
return res.status(400).json({ error: 'Invalid purpose' });
}
const token = createEphemeralToken(authReq.user.id, purpose);
if (!token) return res.status(503).json({ error: 'Service unavailable' });
res.json({ token });
});
export default router;