GraphQL API’da CORS Yapılandırması

Modern web uygulamalarında GraphQL API’ları giderek daha yaygın hale geliyor. Ama ne kadar güçlü bir API tasarlarsanız tasarlayın, CORS yapılandırmasını doğru yapmazsanız frontend uygulamalarınız o API’ye erişemez ya da daha kötüsü, güvenlik açıkları doğurursunuz. Bu yazıda GraphQL API’larınızda CORS’u nasıl doğru yapılandıracağınızı, yaygın hataları nasıl önleyeceğinizi ve production ortamı için en iyi pratikleri ele alacağız.

CORS Nedir ve GraphQL’de Neden Önemlidir

CORS (Cross-Origin Resource Sharing), tarayıcıların farklı origin’lerden gelen istekleri nasıl ele alacağını belirleyen bir güvenlik mekanizmasıdır. Örneğin https://app.sirketim.com adresindeki frontend uygulamanız https://api.sirketim.com adresindeki GraphQL API’nıza istek atmaya çalıştığında, tarayıcı önce bir “preflight” isteği gönderir. Sunucu bu isteğe doğru CORS başlıklarıyla yanıt vermezse tarayıcı asıl isteği engelleyerek bir hata fırlatır.

GraphQL’de CORS meselesi biraz daha karmaşık bir boyut kazanıyor. REST API’lardan farklı olarak GraphQL genellikle tek bir endpoint üzerinden çalışır (/graphql). Bu endpoint hem query hem mutation hem de subscription isteklerini karşılar. Üstelik GraphQL Playground veya Apollo Studio gibi araçlarla geliştirme yaparken farklı origin’lerden istek atmanız çok sık karşılaşılan bir durumdur.

Bir diğer kritik nokta şu: GraphQL mutations, veritabanında veri değiştiren işlemler yapabilir. Eğer CORS’u çok geniş tutarsanız kötü niyetli siteler kullanıcılarınız adına mutation istekleri gönderebilir. Bu da CSRF (Cross-Site Request Forgery) saldırılarına kapı aralar.

Temel CORS Başlıkları

Doğru yapılandırma yapabilmek için önce hangi HTTP başlıklarıyla iş yaptığımızı anlamamız gerekiyor.

  • Access-Control-Allow-Origin: Hangi origin’lerin bu kaynağa erişebileceğini belirtir. * yazmak tüm origin’lere kapıyı açar; bu production’da genellikle kabul edilemez.
  • Access-Control-Allow-Methods: İzin verilen HTTP metodlarını listeler. GraphQL için en azından POST ve OPTIONS gereklidir.
  • Access-Control-Allow-Headers: İstemcinin gönderebileceği başlıkları tanımlar. Content-Type ve Authorization en yaygın ihtiyaç duyulanlardır.
  • Access-Control-Allow-Credentials: Cookie veya Authorization başlığının cross-origin isteklerde iletilip iletilmeyeceğini kontrol eder.
  • Access-Control-Max-Age: Preflight yanıtının tarayıcı tarafından kaç saniye önbelleğe alınacağını belirtir.
  • Access-Control-Expose-Headers: Tarayıcının JavaScript koduna açabileceği yanıt başlıklarını tanımlar.

Node.js ve Express ile GraphQL CORS Yapılandırması

En yaygın kullanılan setup’lardan biri Express üzerinde Apollo Server çalıştırmaktır. Basit bir yapılandırmadan başlayalım:

npm install apollo-server-express express cors
# server.js - Temel CORS yapılandırması
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const cors = require('cors');

const app = express();

// İzin verilen origin listesi
const allowedOrigins = [
  'https://app.sirketim.com',
  'https://admin.sirketim.com',
  'http://localhost:3000',
  'http://localhost:4000'
];

const corsOptions = {
  origin: function (origin, callback) {
    // Origin yoksa (curl gibi araçlar veya same-origin istekler)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error(`CORS policy: ${origin} bu API'ye erişemez`));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Info'],
  maxAge: 86400 // 24 saat
};

// CORS middleware'ini Apollo Server'dan önce uygula
app.use(cors(corsOptions));
app.options('*', cors(corsOptions)); // Preflight istekleri için

async function startServer() {
  const server = new ApolloServer({
    typeDefs,
    resolvers,
    // Apollo Server'ın kendi CORS'unu devre dışı bırak
    // çünkü biz Express seviyesinde yönetiyoruz
  });

  await server.start();
  
  server.applyMiddleware({
    app,
    path: '/graphql',
    cors: false // Express'teki cors middleware'ini kullan
  });

  app.listen(4000, () => {
    console.log('GraphQL API 4000 portunda çalışıyor');
  });
}

startServer();

Burada dikkat etmeniz gereken kritik nokta cors: false ayarıdır. Apollo Server’ın kendi CORS yönetimini devre dışı bırakıp işi Express middleware’ine bırakıyoruz. İkisini aynı anda aktif tutarsanız çakışmalar yaşabilirsiniz.

Apollo Server v4 ile Standalone Yapılandırma

Apollo Server’ın dördüncü sürümüyle birlikte standalone server yapısı geldi. Bu yapıda CORS yapılandırması biraz farklı:

# Apollo Server v4 standalone CORS yapılandırması
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => {
    // Context oluşturma
    const token = req.headers.authorization || '';
    return { token };
  },
});

// Standalone server ile CORS için node-http-server kullanımı
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const express = require('express');
const http = require('http');
const cors = require('cors');

const app = express();
const httpServer = http.createServer(app);

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});

await server.start();

app.use(
  '/graphql',
  cors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
    credentials: true,
  }),
  express.json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({ token: req.headers.authorization }),
  }),
);

await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`GraphQL API çalışıyor: http://localhost:4000/graphql`);

Ortam Bazlı Dinamik CORS Yapılandırması

Production, staging ve development ortamlarının farklı CORS politikalarına ihtiyacı var. Bu durumu environment variable’larla yönetmek en temiz çözüm:

# .env.development
NODE_ENV=development
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4000,http://localhost:8080
CORS_MAX_AGE=600

# .env.staging
NODE_ENV=staging
ALLOWED_ORIGINS=https://staging-app.sirketim.com,https://staging-admin.sirketim.com
CORS_MAX_AGE=3600

# .env.production
NODE_ENV=production
ALLOWED_ORIGINS=https://app.sirketim.com,https://admin.sirketim.com
CORS_MAX_AGE=86400
# cors-config.js - Ortam bazlı CORS yapılandırması
const getCorsConfig = () => {
  const isDevelopment = process.env.NODE_ENV === 'development';
  const allowedOrigins = process.env.ALLOWED_ORIGINS
    ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
    : [];

  return {
    origin: (origin, callback) => {
      // Development ortamında localhost'a her zaman izin ver
      if (isDevelopment && (!origin || origin.startsWith('http://localhost'))) {
        return callback(null, true);
      }

      if (!origin) {
        // Server-to-server istekleri veya Postman gibi araçlar
        return callback(null, true);
      }

      if (allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        console.warn(`CORS ihlali: ${origin} adresinden istek reddedildi`);
        callback(new Error('CORS politikası ihlal edildi'));
      }
    },
    credentials: true,
    methods: ['POST', 'OPTIONS'],
    allowedHeaders: [
      'Content-Type',
      'Authorization',
      'X-Requested-With',
      'X-Apollo-Operation-Name',
      'Apollo-Require-Preflight'
    ],
    maxAge: parseInt(process.env.CORS_MAX_AGE || '3600'),
    optionsSuccessStatus: 200
  };
};

module.exports = { getCorsConfig };

GraphQL Subscription’larında CORS

WebSocket tabanlı subscription’lar için CORS biraz farklı çalışır. WebSocket protokolü, HTTP CORS kurallarına tabi değildir; bunun yerine Origin başlığını kontrol etmeniz gerekir:

# WebSocket subscription CORS yapılandırması
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { execute, subscribe } = require('graphql');

const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
  // WebSocket bağlantılarında origin kontrolü
  verifyClient: (info, callback) => {
    const origin = info.origin;
    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
    
    if (!origin || allowedOrigins.includes(origin)) {
      callback(true);
    } else {
      console.warn(`WebSocket CORS ihlali: ${origin}`);
      callback(false, 403, 'Forbidden: CORS politikası');
    }
  }
});

const serverCleanup = useServer(
  {
    schema,
    context: async (ctx) => {
      // WebSocket context'inde token doğrulama
      const token = ctx.connectionParams?.authorization;
      if (!token) {
        throw new Error('Yetkilendirme gerekli');
      }
      return { token };
    },
    onConnect: async (ctx) => {
      console.log(`WebSocket bağlantısı: ${ctx.extra.request.headers.origin}`);
    },
  },
  wsServer
);

Nginx ile GraphQL CORS Yapılandırması

Pek çok production ortamında uygulama sunucusunun önünde Nginx çalışır. CORS’u Nginx seviyesinde yönetmek hem performanslı hem de merkezi bir çözüm sunar:

# /etc/nginx/sites-available/graphql-api.conf

server {
    listen 443 ssl http2;
    server_name api.sirketim.com;

    ssl_certificate /etc/letsencrypt/live/api.sirketim.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.sirketim.com/privkey.pem;

    # CORS değişkenlerini tanımla
    set $cors_origin "";
    set $cors_methods "POST, OPTIONS";
    set $cors_headers "Content-Type, Authorization, X-Requested-With";

    # İzin verilen origin kontrolü
    if ($http_origin ~* "^https://(app|admin).sirketim.com$") {
        set $cors_origin $http_origin;
    }

    location /graphql {
        # Preflight OPTIONS isteği
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $cors_origin always;
            add_header 'Access-Control-Allow-Credentials' 'true' always;
            add_header 'Access-Control-Allow-Methods' $cors_methods always;
            add_header 'Access-Control-Allow-Headers' $cors_headers always;
            add_header 'Access-Control-Max-Age' 86400 always;
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        # Asıl istek için CORS başlıkları
        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Expose-Headers' 'X-Total-Count' always;

        proxy_pass http://localhost:4000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_cache_bypass $http_upgrade;
    }
}

CORS Hatalarını Test Etme ve Debug

CORS sorunlarını tespit etmek için birkaç pratik yöntem var:

# Preflight isteğini manuel test etme
curl -v -X OPTIONS 
  -H "Origin: https://app.sirketim.com" 
  -H "Access-Control-Request-Method: POST" 
  -H "Access-Control-Request-Headers: Content-Type, Authorization" 
  https://api.sirketim.com/graphql

# Gerçek bir GraphQL isteğini test etme
curl -v -X POST 
  -H "Origin: https://app.sirketim.com" 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer TOKEN_BURAYA" 
  -d '{"query": "{ __typename }"}' 
  https://api.sirketim.com/graphql

# CORS başlıklarını kontrol etme
curl -I -X POST 
  -H "Origin: https://izinsiz-site.com" 
  -H "Content-Type: application/json" 
  https://api.sirketim.com/graphql

CORS hatalarını loglara yazdırmak için özel bir middleware ekleyebilirsiniz:

# cors-logger.js - CORS hata loglama middleware'i
const corsLogger = (req, res, next) => {
  const origin = req.headers.origin;
  const method = req.method;
  
  res.on('finish', () => {
    const corsHeader = res.getHeader('Access-Control-Allow-Origin');
    
    if (origin && !corsHeader) {
      console.error({
        timestamp: new Date().toISOString(),
        type: 'CORS_VIOLATION',
        origin: origin,
        method: method,
        path: req.path,
        statusCode: res.statusCode
      });
    }
    
    if (method === 'OPTIONS') {
      console.log({
        timestamp: new Date().toISOString(),
        type: 'PREFLIGHT',
        origin: origin,
        allowedOrigin: corsHeader,
        statusCode: res.statusCode
      });
    }
  });
  
  next();
};

module.exports = corsLogger;

Güvenlik En İyi Pratikleri

CORS yapılandırmasında güvenlik açısından dikkat etmeniz gereken bazı kritik noktalar var:

  • Wildcard kullanmaktan kaçının: Production’da Access-Control-Allow-Origin: * kullanmak çok tehlikelidir, özellikle credentials: true ile birlikte kullanılamaz zaten ama kötü alışkanlık yaratır.
  • Origin listesini düzenli güncelleyin: Uygulamadan ayrılan veya değişen domain’leri hemen listeden çıkarın.
  • Regex ile origin doğrulamada dikkatli olun: sirketim.com regex’i kötüsirketim.com adresini de geçirebilir. Regex’lerinizi sıkı tutun.
  • Credentials ile birlikte wildcard kullanılamaz: credentials: true ayarladığınızda Access-Control-Allow-Origin: * geçersiz olur, tarayıcı isteği reddeder.
  • Apollo’nun introspection’ını production’da kapatın: CORS’tan bağımsız olarak production ortamında schema bilgisinin açıkta olması güvenlik riski taşır.
  • Rate limiting ekleyin: CORS politikanızı geçen kaynaklardan gelen istekler için de rate limiting uygulamak şarttır.

Gerçek Dünya Senaryosu: Microservice Mimarisinde CORS

Birden fazla frontend uygulamasının tek bir GraphQL gateway’e bağlandığı bir microservice mimarisini düşünün. Bu senaryoda merkezi bir CORS konfigürasyonu oluşturmak mantıklıdır:

# central-cors-config.js
// Redis veya config service'ten yüklenebilir
const loadAllowedOrigins = async () => {
  // Gerçek senaryoda bu liste bir config servisinden veya
  // Redis'ten dinamik olarak yüklenebilir
  const staticOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').map(o => o.trim());
  
  // Wildcard subdomain desteği
  const allowedPatterns = [
    /^https://[w-]+.sirketim.com$/,
    /^https://[w-]+.staging.sirketim.com$/,
  ];
  
  return {
    staticOrigins,
    allowedPatterns
  };
};

const createDynamicCorsMiddleware = async () => {
  const { staticOrigins, allowedPatterns } = await loadAllowedOrigins();
  
  return cors({
    origin: (origin, callback) => {
      if (!origin) return callback(null, true);
      
      // Statik liste kontrolü
      if (staticOrigins.includes(origin)) {
        return callback(null, true);
      }
      
      // Pattern kontrolü
      const isAllowedPattern = allowedPatterns.some(pattern => pattern.test(origin));
      if (isAllowedPattern) {
        return callback(null, true);
      }
      
      callback(new Error(`${origin} CORS politikasınca reddedildi`));
    },
    credentials: true,
    methods: ['POST', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Client-Name', 'X-Client-Version'],
    maxAge: 3600
  });
};

module.exports = { createDynamicCorsMiddleware };

Sonuç

GraphQL API’larında CORS yapılandırması ilk bakışta basit gibi görünse de, yanlış yapıldığında ciddi güvenlik açıklarına veya çalışmayan uygulamalara yol açar. En temel kural şu: her zaman en kısıtlayıcı politikadan başlayın, ihtiyaç duydukça genişletin.

Production ortamı için özet kontrol listesi:

  • Wildcard origin kullanmayın, her zaman belirli origin’leri listeleyin
  • Apollo Server’ın CORS’unu kapatıp Express veya Nginx seviyesinde yönetin
  • Environment variable’larla her ortam için farklı CORS konfigürasyonu oluşturun
  • WebSocket subscription’larında verifyClient ile origin kontrolü yapın
  • CORS ihlallerini loglayın, böylece kimin nereye erişmeye çalıştığını görün
  • Preflight isteklerini curl ile test edin her deploy sonrasında
  • Nginx üzerinden CORS yönetimini değerlendirin, uygulama kodunu sadeleştirir

CORS’u doğru yapılandırmak bir kez yapılıp unutulacak bir iş değil. Yeni frontend uygulaması eklendiğinde, domain değiştiğinde veya yeni bir ortam kurulduğunda mutlaka CORS konfigürasyonunu güncellemeniz gerekiyor. Bunu otomatize etmek için config servislerine yatırım yapmak, özellikle büyük ekiplerde çok işe yarıyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir