immich-app/immich · error · UnauthorizedException

Invalid user token

Error message

Invalid user token

What it means

Thrown at the tail of AuthService.validateSession when no valid session could be built from the supplied token+headers. The token is SHA-256 hashed and matched to a session record; if the session/user cannot be resolved, all earlier return paths are skipped and execution reaches this catch-all.

Source

Thrown at server/src/services/auth.service.ts:585

        hasElevatedPermission = pinExpiresAt > now;

        if (hasElevatedPermission && now.plus({ minutes: 5 }) > pinExpiresAt) {
          await this.sessionRepository.update(session.id, {
            pinExpiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate(),
          });
        }
      }

      return {
        user: session.user,
        session: {
          id: session.id,
          hasElevatedPermission,
        },
      };
    }

    throw new UnauthorizedException('Invalid user token');
  }

  async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise<void> {
    if (!auth.session) {
      throw new BadRequestException('This endpoint can only be used with a session token');
    }

    const user = await this.userRepository.getForPinCode(auth.user.id);
    this.validatePinCode(user, { pinCode: dto.pinCode });

    await this.sessionRepository.update(auth.session.id, {
      pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(),
    });
  }

  async lockSession(auth: AuthDto): Promise<void> {
    if (!auth.session) {
      throw new BadRequestException('This endpoint can only be used with a session token');

View on GitHub (pinned to 199723261c)

Solutions

  1. Have the client re-authenticate (log in again) to obtain a fresh session token.
  2. Verify the token is transmitted intact (no truncation/encoding by a reverse proxy or cookie size limit).
  3. Confirm the sessions table is intact on this instance (not wiped by a restore/migration).
  4. Treat the 401 as a trigger to clear the local session and redirect to login.

Example fix

// before
const auth = await authService.validateSession(token, headers);

// after
let auth: AuthDto;
try {
  auth = await authService.validateSession(token, headers);
} catch (e) {
  if (e instanceof UnauthorizedException) {
    // session is gone — force a clean re-login rather than retrying the dead token
    await sessionStore.clear();
    throw new UnauthorizedException('Session expired, please log in again');
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!token || token.length < 32) {
  throw new UnauthorizedException('Session token is missing');
}

Type guard

function isSessionToken(v: unknown): v is string {
  return typeof v === 'string' && /^[A-Za-z0-9+/=]{32,}$/.test(v);
}

Try / catch

try {
  auth = await authService.validateSession(token, headers);
} catch (e) {
  if (e instanceof UnauthorizedException) {
    await sessionStore.clear(); // dead token — purge locally
    throw new UnauthorizedException('Session expired; please log in again');
  }
  throw e;
}

Prevention

When it happens

Trigger: A request authenticated with a session cookie/bearer token that does not hash to an existing session: logged-out token, session invalidated server-side (e.g. password change / admin forced logout), token from another instance, or a malformed token.

Common situations: Stale browser cookie after the user changed their password or an admin invalidated sessions; token copied from a different deployment; cookie truncated by a proxy; session expired and was reaped.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/f2a0d1aef69274f9. Report an issue: GitHub.