immich-app/immich · error · UnauthorizedException

Invalid token

Error message

Invalid token

What it means

An UnauthorizedException (HTTP 401) thrown by the catch-all in WorkflowExecutionService.validate when verifyJwt throws or any prior validation step fails. This is the umbrella auth failure for plugin-supplied tokens: wrong secret, expired token, malformed JWT, or a verifyJwt exception of any kind.

Source

Thrown at server/src/services/workflow-execution.service.ts:288

      this.logger.warn(`Failed to import plugin from ${folder}:`);
    }
  }

  private validate(authToken: string): AuthDto {
    try {
      const jwt = this.cryptoRepository.verifyJwt<{ userId: string }>(authToken, this.jwtSecret);
      if (!jwt.userId) {
        throw new UnauthorizedException('Invalid token: missing userId');
      }

      return {
        user: {
          id: jwt.userId,
        },
      } as AuthDto;
    } catch (error) {
      this.logger.error('Token validation failed:', error);
      throw new UnauthorizedException('Invalid token');
    }
  }

  private sign(userId: string) {
    return this.cryptoRepository.signJwt({ userId }, this.jwtSecret);
  }

  @OnEvent({ name: 'AssetCreate' })
  onAssetCreate({ asset: { ownerId: userId, id: assetId } }: ArgOf<'AssetCreate'>) {
    return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate });
  }

  @OnEvent({ name: 'AssetMetadataExtracted' })
  onAssetMetadataExtracted({ userId, assetId, source }: ArgOf<'AssetMetadataExtracted'>) {
    // prevent loops
    // TODO loop detection in job service directly
    if (source === 'sidecar-write') {
      return;

View on GitHub (pinned to 199723261c)

Solutions

  1. Have the plugin obtain a fresh authToken from the WorkflowEventPayload.workflow.authToken on each run rather than caching it across restarts.
  2. Restart any long-lived worker that holds tokens after a microservices restart.
  3. Ensure NTP/time sync is correct on all nodes to avoid spurious exp failures.
  4. Confirm only one Immich microservices instance is generating the jwtSecret for the cluster.

Example fix

// before (plugin)
const token = localStorage.getItem('cachedToken'); // stale across restarts

// after
const token = payload.workflow.authToken; // fresh per run
Defensive patterns

Strategy: try-catch

Validate before calling

// No reliable client prediction; tokens are server-issued and short-lived per boot.
// Plugin should always read a fresh authToken from the payload per run.

Type guard

const isInvalidTokenError = (e: unknown): boolean =>
  typeof e === 'object' && e !== null && (e as any).status === 401 && (e as any).message === 'Invalid token';

Try / catch

// Inside plugin host call dispatch, any 401 means abort the run
if (!result.success && result.status === 401) {
  logger.error('Workflow token invalid; obtain fresh authToken from payload');
  return JobStatus.Failed;
}

Prevention

When it happens

Trigger: A plugin passes an authToken that is expired, signed with the wrong secret, truncated, or not a valid JWT at all. Because the jwtSecret is a random 32-byte value regenerated on each microservices boot (onPluginLoad), tokens from a previous boot are invalid.

Common situations: Microservices restarted after a long-running workflow started, invalidating in-flight tokens; plugin hardcodes a token; token copied from a different instance; clock skew causing expiry.

Understand the failure class

Related errors


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