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
- 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.
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
- 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.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid token: missing userId
- Plugin not found
- authToken is required
- Unknown method ${step.method}
- Method "${step.method}" is incompatible with workflow trigge
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/5e125b2e88eaa962.
Report an issue: GitHub.