{"record":{"id":"fb90b8f24f4d5fe7","repo":"actualbudget/actual","slug":"token-expired","errorCode":"token-expired","errorMessage":"token-expired","messagePattern":"token-expired","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"packages/sync-server/src/util/validate-user.ts","lineNumber":33,"sourceCode":"  }\n\n  const session = getSession(token);\n\n  if (!session) {\n    res.status(401);\n    res.send({\n      status: 'error',\n      reason: 'unauthorized',\n      details: 'token-not-found',\n    });\n    return null;\n  }\n\n  if (\n    session.expires_at !== TOKEN_EXPIRATION_NEVER &&\n    session.expires_at * MS_PER_SECOND <= Date.now()\n  ) {\n    res.status(401);\n    res.send({\n      status: 'error',\n      reason: 'token-expired',\n    });\n    return null;\n  }\n\n  return session;\n}\n\nexport function validateAuthHeader(req: Request) {\n  // fallback to trustedProxies when trustedAuthProxies not set\n  const trustedAuthProxies: string[] =\n    config.get('trustedAuthProxies') ?? config.get('trustedProxies');\n  // ensure the first hop from our server is trusted\n  const peer = req.socket.remoteAddress;\n  if (peer === undefined) {\n    console.error(`Header Auth Login attempted but there was no defined peer.`);","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/util/validate-user.ts#L15-L51","documentation":"The sync server validates the session's expiry: sessions carry an expires_at timestamp (unix seconds), and TOKEN_EXPIRATION_NEVER (-1) marks never-expiring tokens. If expires_at is not -1 and expires_at * 1000 is at or before the current time, validateSession returns 401 with reason 'token-expired'. The token is known to the server but its lifetime has elapsed.","triggerScenarios":"Any authenticated sync-server request where the stored session's expires_at (unix seconds) is <= now and not -1. Happens when a client keeps a token longer than the server's configured session expiration (openId/header-auth token lifetimes or the server's token expiration setting) and reuses it after expiry.","commonSituations":"Desktop or mobile app left open across a long period with an expired token; clocks skewed between client and server making a valid token look expired; server-side token_expiration_int set to a short duration (e.g. 3600s) while the app expects long-lived sessions.","solutions":["Re-authenticate to get a fresh token (log in again via /login or /openid/login); the client should handle 401 reason 'token-expired' by refreshing automatically.","Increase the server's token expiration setting (token_expiration_int in the config / TOKEN_EXPIRATION env) if sessions expire too quickly.","Verify server clock (NTP) — a fast clock invalidates tokens early.","For long-lived automation/API usage, configure never-expiring behavior where appropriate (expires_at = -1) or implement token refresh."],"exampleFix":"// before: blindly reusing stored token\nconst res = await fetch(url + '/sync', { headers: { 'x-actual-token': storedToken } });\n// after: handle expiry by re-authenticating\nlet res = await fetch(url + '/sync', { headers: { 'x-actual-token': storedToken } });\nif (res.status === 401) {\n  storedToken = await login(password); // refresh and retry\n  res = await fetch(url + '/sync', { headers: { 'x-actual-token': storedToken } });\n}","handlingStrategy":"retry","validationCode":"const expiresAtSec = session?.expires_at;\nif (expiresAtSec !== -1 && expiresAtSec * 1000 <= Date.now()) {\n  throw new Error('token expired; re-authenticate before calling the sync server');\n}","typeGuard":"function isSessionValid(s: { expires_at: number } | null): boolean {\n  return s != null && (s.expires_at === -1 || s.expires_at * 1000 > Date.now());\n}","tryCatchPattern":"try {\n  const res = await fetch(url + '/sync', { headers: { 'x-actual-token': token } });\n  if (res.status === 401 && (await res.json()).reason === 'token-expired') {\n    token = await login(credentials); // refresh token, then retry\n    return await syncRequest(token);\n  }\n} catch (e) {\n  logger.error('sync failed after token refresh', e);\n}","preventionTips":["Check expires_at locally before each request and refresh proactively.","Sync client clocks with NTP to avoid premature expiry.","Set an appropriate token_expiration_int on the server for your usage pattern.","Handle 401 reason 'token-expired' with automatic re-login in your HTTP client wrapper."],"tags":["auth","sync-server","http-401","token-expiry"],"backgroundTag":"session-token-expired","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}