immich-app/immich · error · ForbiddenException

Missing required permission

Error message

Missing required permission: ${requestedPermission}

What it means

Thrown by authenticate() when an API-key-authenticated request does not carry the permission required for the requested operation. isGranted() compares the requested permission against the key's stored permission set and fails the request with 403.

Solutions

  1. Regenerate or edit the API key in Immich user settings, granting the required permission
  2. Recreate the key and update the client's x-api-key header
  3. Verify you are calling the intended endpoint/version for your key's scope

Example fix

// before
curl -H 'x-api-key: OLD_KEY' /api/library
// after
// regenerate key with required permission in Immich settings
curl -H 'x-api-key: NEW_KEY' /api/library
Defensive patterns

Strategy: try-catch

Validate before calling

if (!keyPermissions.includes(requiredPermission)) throw new Error('API key lacks ' + requiredPermission);

Try / catch

try { await api.request(...) } catch (e) { if (e.status === 403 && /Missing required permission/.test(e.message)) { /* prompt to grant key permission */ } throw e; }

Prevention

When it happens

Trigger: A request authenticates with an API key (authDto.apiKey set), requestedPermission is not false, and the key's permissions lack the requested permission.

Common situations: Using an Immich API key created without the needed scope (e.g. asset-scoped key used for library or admin operations); keys from older versions missing new permission slugs; sharing one key across many integrations.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/5d9ece26c6055f27. Report an issue: GitHub.

Appendix: source

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

    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 f48d4b3321)