immich-app/immich · error · UnauthorizedException

Invalid JWT Token

Error message

Invalid JWT Token

What it means

This UnauthorizedException is thrown when jwtVerify fails inside maintenanceWorkerService.login(): the JWT was supplied but its signature does not match the HMAC of this.worker.secret, it is malformed, or it is expired. It means the token cannot be trusted as a MaintenanceAuthDto payload.

Solutions

  1. Re-perform the maintenance login to get a freshly signed token and retry.
  2. Do not include the 'Bearer ' prefix when passing the raw JWT string.
  3. Ensure the request goes to the same server instance that issued the token (same secret).
  4. If tokens keep failing after restarts, make the maintenance secret persistent/stable across restarts instead of regenerating per boot.
  5. Check client/server clock synchronization if exp-based rejection is suspected.

Example fix

// before
const token = authHeader; // "Bearer eyJhbGci..."
await worker.login(token); // Invalid JWT Token
// after
const token = authHeader.replace(/^Bearer\s+/i, '');
await worker.login(token);
Defensive patterns

Strategy: try-catch

Validate before calling

if (!/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/.test(token)) {
  throw new Error('Malformed JWT; re-login to obtain a valid maintenance token');
}

Try / catch

try {
  await worker.login(token);
} catch (e) {
  if (e instanceof UnauthorizedException && e.message === 'Invalid JWT Token') {
    // clear stored token and re-authenticate
    await worker.login(await obtainFreshToken());
  } else throw e;
}

Prevention

When it happens

Trigger: Calling login() with a JWT signed with a different secret (e.g. server regenerated its secret between sessions); token expired per its exp claim; corrupted/truncated token or one signed by the main Immich auth service instead of the maintenance worker; token string prefixed (e.g. 'Bearer x') and passed whole to jwtVerify.

Common situations: Restarting the maintenance server after it generated a new random secret while the browser still holds an old cookie/token; copying a token from another instance/environment; clock skew making a recently issued token appear expired; storing and replaying a token across version upgrades that changed signing details.

Related errors


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

Appendix: source

Thrown at server/src/maintenance/maintenance-worker.service.ts:269

    } catch {
      return this.getPublicStatus();
    }
  }

  detectPriorInstall(): Promise<MaintenanceDetectInstallResponseDto> {
    return detectPriorInstall(this.storageRepository);
  }

  async login(jwt?: string): Promise<MaintenanceAuthDto> {
    if (!jwt) {
      throw new UnauthorizedException('Missing JWT Token');
    }

    try {
      const result = await jwtVerify<MaintenanceAuthDto>(jwt, new TextEncoder().encode(this.secret));
      return result.payload;
    } catch {
      throw new UnauthorizedException('Invalid JWT Token');
    }
  }

  async setAction(action: SetMaintenanceModeDto) {
    this.setStatus({
      active: true,
      action: action.action,
    });

    await this.runAction(action);
  }

  async runAction(action: SetMaintenanceModeDto) {
    switch (action.action) {
      case MaintenanceAction.Start:
      case MaintenanceAction.SelectDatabaseRestore: {
        return;
      }

View on GitHub (pinned to e55ac299a4)