immich-app/immich · error · UnauthorizedException

Invalid JWT Token

Error message

Invalid JWT Token

What it means

After a JWT is present, login runs jwtVerify against this.secret. Any verification failure (bad signature, expired, malformed, wrong audience) is swallowed and rethrown as UnauthorizedException('Invalid JWT Token'). The original cause is intentionally hidden to avoid leaking token-validation internals.

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 199723261c)

Solutions

  1. Re-issue the maintenance JWT against the current server secret.
  2. Verify system clock/NTP is correct on both client and server to avoid spurious exp/nbf failures.
  3. Confirm the token was generated by the same Immich instance/secret it is being verified against.
Defensive patterns

Strategy: try-catch

Validate before calling

import jwtDecode from 'jwt-decode';
const decoded = jwtDecode<{ exp?: number }>(jwt);
if (decoded.exp && decoded.exp * 1000 < Date.now()) {
  throw new Error('Maintenance token expired; re-issue it.');
}

Type guard

const looksLikeJwt = (s: string): boolean =>
  typeof s === 'string' && s.split('.').length === 3;

Try / catch

try {
  await login(jwt);
} catch (e) {
  if (/Invalid JWT/.test((e as Error).message)) {
    // re-issue token from the current server secret and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Submitting a maintenance JWT that is expired, signed with a different secret, truncated, or otherwise malformed to the maintenance login.

Common situations: Server secret rotated since the token was issued; clock skew between client and server causing premature expiry; token copy-paste error; token from a different Immich instance.

Related errors


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