immich-app/immich · error · ForbiddenException

Forbidden

Error message

Forbidden

What it means

ForbiddenException (HTTP 403) thrown by AuthService.authenticate when metadata.adminRoute is true and the resolved user is not an admin. The auth guard attaches adminRoute metadata to admin-only endpoints; non-admins are rejected before the handler runs. A warn log records the denied URI for auditing.

Source

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

    const admin = await this.createUser({
      isAdmin: true,
      email: dto.email,
      name: dto.name,
      password: dto.password,
      storageLabel: 'admin',
    });

    return mapUserAdmin(admin);
  }

  async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise<AuthDto> {
    const authDto = await this.validate({ headers, queryParams });
    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;
  }

View on GitHub (pinned to 199723261c)

Solutions

  1. Use an admin user's session token/API key for admin endpoints.
  2. If the user was recently promoted, log out and back in to refresh the cached isAdmin flag.
  3. Gate the admin UI behind an isAdmin check before issuing the request.
  4. Check server logs for the warn line 'Denied access to admin only route' to identify the offending token.

Example fix

// before
const client = axios.create({ headers: { Authorization: `Bearer ${userToken}` } });
await client.delete('/admin/users/123');

// after
const me = await api.userApi.getMyUserInfo();
if (!me.isAdmin) throw new Error('admin required');
const client = axios.create({ headers: { Authorization: `Bearer ${adminToken}` } });
await client.delete('/admin/users/123');
Defensive patterns

Strategy: validation

Validate before calling

async function isAdmin(token: string): Promise<boolean> {
  const { data } = await axios.get('/users/me', { headers: { Authorization: `Bearer ${token}` } });
  return data.isAdmin === true;
}

Type guard

function isAdminUser(user: { isAdmin?: boolean }): user is { isAdmin: true } {
  return user.isAdmin === true;
}

Try / catch

try {
  await axios.delete(`/admin/users/${id}`, { headers: auth() });
} catch (e) {
  if (e.response?.status === 403) {
    showInsufficientPrivileges();
  } else throw e;
}

Prevention

When it happens

Trigger: Any request to an admin-only route (e.g. DELETE /admin/users/:id, POST /system/preferences) carrying a valid non-admin session/API-key/shared-link token. The token authenticates successfully but authorization fails at the adminRoute check.

Common situations: Normal user token reused against admin endpoints after a role demotion; client uses the wrong account; shared link or API key with insufficient privileges hitting admin endpoints; JWT not refreshed after an admin role change.

Understand the failure class

Related errors


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