immich-app/immich · error · ForbiddenException

Missing required permission: ${requestedPermission}

Error message

Missing required permission: ${requestedPermission}

What it means

ForbiddenException (HTTP 403) thrown by AuthService.authenticate when the request carries an API key and the route's requested permission (defaults to Permission.All when metadata.permission is unset) is not granted by the key's permissions via isGranted. The message interpolates the missing permission name to aid debugging.

Source

Thrown at server/src/services/auth.service.ts:231

    const { adminRoute, sharedLinkRoute, uri } = metadata;
    const requestedPermission = metadata.permission ?? Permission.All;

    if (!authDto.user.isAdmin && adminRoute) {
      this.logger.warn(`Denied access to admin only route: ${uri}`);
      throw new ForbiddenException('Forbidden');
    }

    if (authDto.sharedLink && !sharedLinkRoute) {
      this.logger.warn(`Denied access to non-shared route: ${uri}`);
      throw new ForbiddenException('Forbidden');
    }

    if (
      authDto.apiKey &&
      requestedPermission !== false &&
      !isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions })
    ) {
      throw new ForbiddenException(`Missing required permission: ${requestedPermission}`);
    }

    return authDto;
  }

  private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> {
    const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string;
    const shareSlug = (headers[ImmichHeader.SharedLinkSlug] || queryParams[ImmichQuery.SharedLinkSlug]) as string;
    const session = (headers[ImmichHeader.UserToken] ||
      headers[ImmichHeader.SessionToken] ||
      queryParams[ImmichQuery.SessionKey] ||
      this.getBearerToken(headers) ||
      this.getCookieToken(headers)) as string;
    const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;

    if (shareKey) {
      return this.validateSharedLinkKey(shareKey);
    }

View on GitHub (pinned to 199723261c)

Solutions

  1. Issue a new API key whose permissions include the value named in the error message.
  2. If the route genuinely needs full access, use a session token instead of an API key.
  3. Have an admin edit the key's permissions in /api-keys settings.
  4. Audit isGranted outputs against the current Permission enum after upgrades.

Example fix

// before
await axios.post('/assets', data, { headers: { 'x-api-key': readOnlyKey } });
// -> 403 Missing required permission: asset.create

// after
const key = await axios.post('/api-keys', { name: 'uploader', permissions: ['asset.create'] }, { headers: { Authorization: `Bearer ${adminSession}` } });
await axios.post('/assets', data, { headers: { 'x-api-key': key.data.secret } });
Defensive patterns

Strategy: validation

Validate before calling

async function keyHasPermission(key: string, perm: string): Promise<boolean> {
  const { data } = await axios.get('/api-keys', { headers: { 'x-api-key': key } });
  return data.permissions?.includes(perm);
}

Type guard

function keyGrants(perms: string[], required: string): boolean {
  return perms.includes(required) || perms.includes('all');
}

Try / catch

try {
  await axios.post('/assets', data, { headers: { 'x-api-key': key } });
} catch (e) {
  if (e.response?.status === 403 && /Missing required permission/.test(e.response?.data?.message || '')) {
    const required = e.response.data.message.split(': ').pop();
    await provisionKeyWithPermission(required);
  } else throw e;
}

Prevention

When it happens

Trigger: Any authenticated request made with an x-api-key whose permission set does not include the route's required Permission. Example: an API key scoped to asset.read hitting POST /assets (asset.create), or any key on a route with no explicit permission (defaults to All).

Common situations: API key created with limited scopes and reused for an admin/general call; route added or upgraded to require a permission the key lacks; client hard-codes a stale API key after permissions were tightened.

Related errors


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