{"record":{"id":"5e125b2e88eaa962","repo":"immich-app/immich","slug":"invalid-token","errorCode":null,"errorMessage":"Invalid token","messagePattern":"Invalid token","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"server/src/services/workflow-execution.service.ts","lineNumber":288,"sourceCode":"      this.logger.warn(`Failed to import plugin from ${folder}:`);\n    }\n  }\n\n  private validate(authToken: string): AuthDto {\n    try {\n      const jwt = this.cryptoRepository.verifyJwt<{ userId: string }>(authToken, this.jwtSecret);\n      if (!jwt.userId) {\n        throw new UnauthorizedException('Invalid token: missing userId');\n      }\n\n      return {\n        user: {\n          id: jwt.userId,\n        },\n      } as AuthDto;\n    } catch (error) {\n      this.logger.error('Token validation failed:', error);\n      throw new UnauthorizedException('Invalid token');\n    }\n  }\n\n  private sign(userId: string) {\n    return this.cryptoRepository.signJwt({ userId }, this.jwtSecret);\n  }\n\n  @OnEvent({ name: 'AssetCreate' })\n  onAssetCreate({ asset: { ownerId: userId, id: assetId } }: ArgOf<'AssetCreate'>) {\n    return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate });\n  }\n\n  @OnEvent({ name: 'AssetMetadataExtracted' })\n  onAssetMetadataExtracted({ userId, assetId, source }: ArgOf<'AssetMetadataExtracted'>) {\n    // prevent loops\n    // TODO loop detection in job service directly\n    if (source === 'sidecar-write') {\n      return;","sourceCodeStart":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/workflow-execution.service.ts#L270-L306","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Have the plugin obtain a fresh authToken from the WorkflowEventPayload.workflow.authToken on each run rather than caching it across restarts.","Restart any long-lived worker that holds tokens after a microservices restart.","Ensure NTP/time sync is correct on all nodes to avoid spurious exp failures.","Confirm only one Immich microservices instance is generating the jwtSecret for the cluster."],"exampleFix":"// before (plugin)\nconst token = localStorage.getItem('cachedToken'); // stale across restarts\n\n// after\nconst token = payload.workflow.authToken; // fresh per run","handlingStrategy":"try-catch","validationCode":"// No reliable client prediction; tokens are server-issued and short-lived per boot.\n// Plugin should always read a fresh authToken from the payload per run.","typeGuard":"const isInvalidTokenError = (e: unknown): boolean =>\n  typeof e === 'object' && e !== null && (e as any).status === 401 && (e as any).message === 'Invalid token';","tryCatchPattern":"// Inside plugin host call dispatch, any 401 means abort the run\nif (!result.success && result.status === 401) {\n  logger.error('Workflow token invalid; obtain fresh authToken from payload');\n  return JobStatus.Failed;\n}","preventionTips":["Never cache authTokens across microservices restarts.","Read authToken fresh from WorkflowEventPayload.workflow.authToken on each run.","Ensure NTP/time sync to avoid spurious exp failures."],"tags":["workflow","plugin","authentication","jwt","nestjs"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}