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

@@ -240,17 +240,19 @@ router.get('/oidc', (_req: Request, res: Response) => {
client_secret_set: !!secret,
display_name: get('oidc_display_name'),
oidc_only: get('oidc_only') === 'true',
discovery_url: get('oidc_discovery_url'),
});
});
router.put('/oidc', (req: Request, res: Response) => {
const { issuer, client_id, client_secret, display_name, oidc_only } = req.body;
const { issuer, client_id, client_secret, display_name, oidc_only, discovery_url } = req.body;
const set = (key: string, val: string) => db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)").run(key, val || '');
set('oidc_issuer', issuer);
set('oidc_client_id', client_id);
if (client_secret !== undefined) set('oidc_client_secret', maybe_encrypt_api_key(client_secret) ?? '');
set('oidc_display_name', display_name);
set('oidc_only', oidc_only ? 'true' : 'false');
set('oidc_discovery_url', discovery_url);
const authReq = req as AuthRequest;
writeAudit({
userId: authReq.user.id,