{"record":{"id":"95ee28f5d0efb3a1","repo":"immich-app/immich","slug":"invalid-token-missing-userid","errorCode":null,"errorMessage":"Invalid token: missing userId","messagePattern":"Invalid token: missing userId","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"server/src/services/workflow-execution.service.ts","lineNumber":278,"sourceCode":"          `Upgraded plugin ${manifest.name} (${plugin.methods.length} methods) from ${existing.version} to ${manifest.version} `,\n        );\n      } else {\n        this.logger.log(\n          `Imported plugin ${manifest.name}@${manifest.version} (${plugin.methods.length} methods) from ${folder}`,\n        );\n      }\n\n      return manifest;\n    } catch {\n      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' })","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/workflow-execution.service.ts#L260-L296","documentation":"An UnauthorizedException (HTTP 401) thrown by WorkflowExecutionService.validate when the JWT verifies (correct signature and secret) but the decoded payload has no userId claim. Because the catch block around it re-wraps any error as 'Invalid token', this specific message is only observable if it escapes the catch (it does not, in the current code, since the catch is broader); treat it as the documented reason for a malformed-but-signed token used by a plugin host function call.","triggerScenarios":"A plugin passes an authToken whose JWT decodes successfully but lacks a userId field, e.g., a token minted for a different purpose or with an alternate claim name. validate() is called from wrap(), so the error is caught and returned to the plugin as a failure response.","commonSituations":"Plugin SDK signs its own token with the wrong claim shape; token from a test harness using { sub } instead of { userId }; schema drift in the JWT payload contract.","solutions":["Ensure any JWT minted for workflow use includes a userId claim matching the Immich user id.","Use the WorkflowExecutionService.sign path (server-side) to mint tokens rather than constructing them manually.","In plugin tests, mint tokens with exactly { userId: '<uuid>' }.","Verify the token secret matches the server's jwtSecret (regenerated on each microservices start)."],"exampleFix":"// before\nconst token = jwt.sign({ sub: userId }, secret); // missing userId claim\n\n// after\nconst token = jwt.sign({ userId }, secret);","handlingStrategy":"validation","validationCode":"// When minting tokens for workflows, include userId\nfunction mintWorkflowToken(userId, secret) {\n  if (!userId) throw new Error('userId required for workflow token');\n  return jwt.sign({ userId }, secret);\n}","typeGuard":"const hasUserIdClaim = (decoded: unknown): decoded is { userId: string } =>\n  typeof decoded === 'object' && decoded !== null && typeof (decoded as any).userId === 'string';","tryCatchPattern":"// validate() catches and re-wraps; on the plugin side, treat any auth failure as fatal\nif (!result.success && (result.status === 401)) {\n  abortWorkflow('auth token rejected by host');\n}","preventionTips":["Mint workflow tokens server-side via WorkflowExecutionService.sign.","Always use { userId } as the JWT payload for plugin tokens.","In tests, mint tokens with exactly { userId: '<uuid>' }."],"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"}