{"record":{"id":"278c2539778decb3","repo":"HeyPuter/puter","slug":"unauthorized-278c25","errorCode":"unauthorized","errorMessage":"User required","messagePattern":"User required","errorType":"exception","errorClass":"HttpError","httpStatus":401,"severity":"error","filePath":"src/backend/core/http/middleware/userProtected.ts","lineNumber":165,"sourceCode":"    options: UserProtectedGateOptions = {},\n): RequestHandler[] => {\n    const { config, userStore, oidcService, tokenService } = deps;\n    const allowTemp = !!options.allowTempUsers;\n\n    // 1. Session cookie only. Shared with the standalone cookie-only gate.\n    const requireSessionCookie = createSessionCookieGate(config);\n\n    // 2. Fresh user row (bypass cache to catch just-suspended accounts).\n    // `getById` doesn't take options; go through `getByProperty` with\n    // `{ force: true }` to force a primary read.\n    const refreshUser: RequestHandler = async (\n        req: Request,\n        _res: Response,\n        next: NextFunction,\n    ) => {\n        const actor = req.actor;\n        if (!actor?.user?.id)\n            throw new HttpError(401, 'User required', {\n                legacyCode: 'unauthorized',\n            });\n        const user = await userStore.getByProperty('id', actor.user.id, {\n            force: true,\n        });\n        if (!user)\n            throw new HttpError(404, 'User not found', {\n                legacyCode: 'not_found',\n            });\n        if (user.suspended)\n            throw new HttpError(403, 'Account is suspended', {\n                legacyCode: 'account_suspended',\n            });\n        req.userProtected = { user };\n        next();\n    };\n\n    // 3. Password (bcrypt) OR valid OIDC revalidation cookie.","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/core/http/middleware/userProtected.ts#L147-L183","documentation":"Raised in the `refreshUser` step of the `userProtected` middleware chain when an actor exists but has no `user.id`. The userProtected gate protects sensitive browser-only operations (delete account, change password) and requires a fully authenticated user actor backed by a web session — anonymous, app-only, or access-token actors do not qualify.","triggerScenarios":"Calling a userProtected route without a logged-in user session: expired session cookie, an API/app token used instead of the web session, or an anonymous request.","commonSituations":"Session expired between page load and the sensitive action; a script tried to call a browser-only route with an API token; the session cookie wasn't sent (cross-origin/CORS/credentials).","solutions":["Authenticate via the web session (browser login) before calling userProtected routes.","Ensure the session cookie is sent with the request (credentials: 'include').","Do not call these routes with API/app tokens — they are browser-session-only by design."],"exampleFix":"// before\nfetch('/user', { method: 'DELETE' }); // no cookie\n// after\nfetch('/user', { method: 'DELETE', credentials: 'include' });","handlingStrategy":"validation","validationCode":"// Only call userProtected routes when a web session is present:\nif (!hasSessionCookie()) { routeToLogin(); return; }","typeGuard":null,"tryCatchPattern":"try { await call({ credentials: 'include' }); }\ncatch (e) {\n  if (e.code === 'unauthorized') { routeToLogin(); return; }\n  throw e;\n}","preventionTips":["Call userProtected routes with the web session cookie (credentials: 'include').","Don't use API/app tokens for browser-session-only routes.","Re-authenticate when the session expires."],"tags":["auth","user-protected","session","unauthorized"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}