HeyPuter/puter · error · HttpError

unauthorized

unauthorized

Error message

User required

What it means

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.

Solutions

  1. Authenticate via the web session (browser login) before calling userProtected routes.
  2. Ensure the session cookie is sent with the request (credentials: 'include').
  3. Do not call these routes with API/app tokens — they are browser-session-only by design.

Example fix

// before
fetch('/user', { method: 'DELETE' }); // no cookie
// after
fetch('/user', { method: 'DELETE', credentials: 'include' });
Defensive patterns

Strategy: validation

Validate before calling

// Only call userProtected routes when a web session is present:
if (!hasSessionCookie()) { routeToLogin(); return; }

Try / catch

try { await call({ credentials: 'include' }); }
catch (e) {
  if (e.code === 'unauthorized') { routeToLogin(); return; }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/278c2539778decb3. Report an issue: GitHub.

Appendix: source

Thrown at src/backend/core/http/middleware/userProtected.ts:165

    options: UserProtectedGateOptions = {},
): RequestHandler[] => {
    const { config, userStore, oidcService, tokenService } = deps;
    const allowTemp = !!options.allowTempUsers;

    // 1. Session cookie only. Shared with the standalone cookie-only gate.
    const requireSessionCookie = createSessionCookieGate(config);

    // 2. Fresh user row (bypass cache to catch just-suspended accounts).
    // `getById` doesn't take options; go through `getByProperty` with
    // `{ force: true }` to force a primary read.
    const refreshUser: RequestHandler = async (
        req: Request,
        _res: Response,
        next: NextFunction,
    ) => {
        const actor = req.actor;
        if (!actor?.user?.id)
            throw new HttpError(401, 'User required', {
                legacyCode: 'unauthorized',
            });
        const user = await userStore.getByProperty('id', actor.user.id, {
            force: true,
        });
        if (!user)
            throw new HttpError(404, 'User not found', {
                legacyCode: 'not_found',
            });
        if (user.suspended)
            throw new HttpError(403, 'Account is suspended', {
                legacyCode: 'account_suspended',
            });
        req.userProtected = { user };
        next();
    };

    // 3. Password (bcrypt) OR valid OIDC revalidation cookie.

View on GitHub (pinned to 908ec23eda)