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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user