immich-app/immich · error · UnauthorizedException

Invalid user token

Error message

Invalid user token

What it means

Session-token authentication resolves the bearer JWT to a session and user. If the token cannot be verified, the session no longer exists, or the stored token hash no longer matches, validateSession throws UnauthorizedException('Invalid user token').

Solutions

  1. Log in again to obtain a fresh access token
  2. Verify JWT_SECRET is identical and stable across restarts and all instances
  3. Check the session was not deleted (logout, admin session revoke)
  4. Sync server clocks / check token expiry handling

Example fix

// before
JWT_SECRET=new-secret  # rotated, invalidates all sessions
// after
JWT_SECRET=<same stable secret across deployments>
Defensive patterns

Strategy: retry

Validate before calling

const payload = decodeJwt(token); if (payload.exp * 1000 < Date.now()) await relogin();

Type guard

const isJwt = (t: string) => t.split('.').length === 3;

Try / catch

catch (e) { if (e.status === 401 && e.message === 'Invalid user token') { await refreshOrRelogin(); } }

Prevention

When it happens

Trigger: Request with an Authorization bearer token that is expired, signed with a different secret, revoked, or whose session row was deleted.

Common situations: JWT_SECRET changed or differs between instances (e.g. after redeploy or multi-node setup without shared secret); user logged out / session cleared; token copied from another environment; clock skew making tokens appear expired.

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/f2a0d1aef69274f9. Report an issue: GitHub.

Appendix: source

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

        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 f48d4b3321)