immich-app/immich · error · UnauthorizedException

Invalid token: missing userId

Error message

Invalid token: missing userId

What it means

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.

Source

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

          `Upgraded plugin ${manifest.name} (${plugin.methods.length} methods) from ${existing.version} to ${manifest.version} `,
        );
      } else {
        this.logger.log(
          `Imported plugin ${manifest.name}@${manifest.version} (${plugin.methods.length} methods) from ${folder}`,
        );
      }

      return manifest;
    } catch {
      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' })

View on GitHub (pinned to 199723261c)

Solutions

  1. Ensure any JWT minted for workflow use includes a userId claim matching the Immich user id.
  2. Use the WorkflowExecutionService.sign path (server-side) to mint tokens rather than constructing them manually.
  3. In plugin tests, mint tokens with exactly { userId: '<uuid>' }.
  4. Verify the token secret matches the server's jwtSecret (regenerated on each microservices start).

Example fix

// before
const token = jwt.sign({ sub: userId }, secret); // missing userId claim

// after
const token = jwt.sign({ userId }, secret);
Defensive patterns

Strategy: validation

Validate before calling

// When minting tokens for workflows, include userId
function mintWorkflowToken(userId, secret) {
  if (!userId) throw new Error('userId required for workflow token');
  return jwt.sign({ userId }, secret);
}

Type guard

const hasUserIdClaim = (decoded: unknown): decoded is { userId: string } =>
  typeof decoded === 'object' && decoded !== null && typeof (decoded as any).userId === 'string';

Try / catch

// validate() catches and re-wraps; on the plugin side, treat any auth failure as fatal
if (!result.success && (result.status === 401)) {
  abortWorkflow('auth token rejected by host');
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


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