mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
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:
@@ -60,22 +60,24 @@ function getOidcConfig() {
|
||||
const clientId = process.env.OIDC_CLIENT_ID || get('oidc_client_id');
|
||||
const clientSecret = process.env.OIDC_CLIENT_SECRET || decrypt_api_key(get('oidc_client_secret'));
|
||||
const displayName = process.env.OIDC_DISPLAY_NAME || get('oidc_display_name') || 'SSO';
|
||||
const discoveryUrl = process.env.OIDC_DISCOVERY_URL || get('oidc_discovery_url') || null;
|
||||
if (!issuer || !clientId || !clientSecret) return null;
|
||||
return { issuer: issuer.replace(/\/+$/, ''), clientId, clientSecret, displayName };
|
||||
return { issuer: issuer.replace(/\/+$/, ''), clientId, clientSecret, displayName, discoveryUrl };
|
||||
}
|
||||
|
||||
let discoveryCache: OidcDiscoveryDoc | null = null;
|
||||
let discoveryCacheTime = 0;
|
||||
const DISCOVERY_TTL = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
async function discover(issuer: string) {
|
||||
if (discoveryCache && Date.now() - discoveryCacheTime < DISCOVERY_TTL && discoveryCache._issuer === issuer) {
|
||||
async function discover(issuer: string, discoveryUrl?: string | null) {
|
||||
const url = discoveryUrl || `${issuer}/.well-known/openid-configuration`;
|
||||
if (discoveryCache && Date.now() - discoveryCacheTime < DISCOVERY_TTL && discoveryCache._issuer === url) {
|
||||
return discoveryCache;
|
||||
}
|
||||
const res = await fetch(`${issuer}/.well-known/openid-configuration`);
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('Failed to fetch OIDC discovery document');
|
||||
const doc = await res.json() as OidcDiscoveryDoc;
|
||||
doc._issuer = issuer;
|
||||
doc._issuer = url;
|
||||
discoveryCache = doc;
|
||||
discoveryCacheTime = Date.now();
|
||||
return doc;
|
||||
@@ -120,7 +122,7 @@ router.get('/login', async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const doc = await discover(config.issuer);
|
||||
const doc = await discover(config.issuer, config.discoveryUrl);
|
||||
const state = crypto.randomBytes(32).toString('hex');
|
||||
const appUrl = process.env.APP_URL || (db.prepare("SELECT value FROM app_settings WHERE key = 'app_url'").get() as { value: string } | undefined)?.value;
|
||||
if (!appUrl) {
|
||||
@@ -172,7 +174,7 @@ router.get('/callback', async (req: Request, res: Response) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const doc = await discover(config.issuer);
|
||||
const doc = await discover(config.issuer, config.discoveryUrl);
|
||||
|
||||
const tokenRes = await fetch(doc.token_endpoint, {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user