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.

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)

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.