Hasura ile JWT Kimlik Doğrulama Entegrasyonu

Modern web uygulamalarında kimlik doğrulama, güvenliğin bel kemiğini oluşturuyor. Hasura kullanıyorsanız ve JWT tabanlı bir auth sistemi kurmak istiyorsanız, doğru yapılandırma hem güvenliği hem de performansı doğrudan etkiliyor. Bu yazıda Hasura’nın JWT kimlik doğrulama mekanizmasını baştan sona ele alacağız. Production ortamında karşılaştığım gerçek senaryolara dayanarak, hem teorik hem de pratik açıdan konuyu derinlemesine işleyeceğiz.

JWT ve Hasura’nın Kimlik Doğrulama Modeli

Hasura, gelen her GraphQL isteğinde HTTP header’larını analiz eder. Kimlik doğrulama için iki temel yöntem sunar: webhook ve JWT. JWT yöntemi, harici bir auth servisine bağımlılık olmadan token doğrulama yapmanızı sağladığı için özellikle mikroservis mimarilerinde tercih edilir.

Hasura’nın JWT modelinde temel mantık şudur: Kullanıcı bir JWT token alır, bu token’ı GraphQL isteklerinde Authorization: Bearer header’ı olarak gönderir, Hasura bu token’ı doğrular ve içindeki claim’lere bakarak hangi role sahip olduğunu anlar. Bu role göre izin kontrolleri devreye girer.

Hasura’nın JWT claim’lerinde beklediği alanlar şunlardır:

  • x-hasura-default-role: Kullanıcının varsayılan rolü
  • x-hasura-allowed-roles: Kullanıcının sahip olabileceği roller dizisi
  • x-hasura-user-id: Kullanıcı kimliği
  • x-hasura-org-id: Organizasyon kimliği (isteğe bağlı özel claim)

Bu claim’ler JWT’nin payload kısmında https://hasura.io/jwt/claims namespace’i altında yer alır.

Ortam Kurulumu

Başlamadan önce Docker Compose ile Hasura ve PostgreSQL’i ayağa kaldıralım. Bu yapı hem local development hem de test ortamları için idealdir.

# docker-compose.yml oluştur
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
  postgres:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_PASSWORD: mysecretpassword
      POSTGRES_DB: hasura_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  hasura:
    image: hasuraio/graphql-engine:v2.35.0
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    restart: always
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:mysecretpassword@postgres:5432/hasura_db
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ADMIN_SECRET: "myadminsecret"
      HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"bu-en-az-32-karakter-olmali-gizli-anahtar-1234"}'
      HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"

volumes:
  postgres_data:
EOF

docker compose up -d

HASURA_GRAPHQL_JWT_SECRET environment değişkeni kritik önem taşıyor. Bu değişkende type olarak HS256, HS384, HS512, RS256 veya RS512 kullanabilirsiniz. Simetrik algoritmalar için key alanı gizli anahtarı tutarken, asimetrik algoritmalar için public key kullanılır.

JWT Token Üretimi

Gerçek dünya senaryosunda JWT token’larını genellikle ayrı bir auth servisi üretir. Node.js ile basit bir token üretici yazalım:

# Node.js auth servisi için gerekli paketleri yükle
mkdir auth-service && cd auth-service
npm init -y
npm install jsonwebtoken express bcryptjs dotenv

# index.js oluştur
cat > index.js << 'EOF'
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
require('dotenv').config();

const app = express();
app.use(express.json());

// Örnek kullanıcı veritabanı (gerçekte PostgreSQL kullanılacak)
const users = [
  {
    id: '1',
    email: '[email protected]',
    password: bcrypt.hashSync('admin123', 10),
    role: 'admin',
    allowed_roles: ['admin', 'user', 'anonymous']
  },
  {
    id: '2',
    email: '[email protected]',
    password: bcrypt.hashSync('user123', 10),
    role: 'user',
    allowed_roles: ['user', 'anonymous']
  }
];

const JWT_SECRET = process.env.JWT_SECRET || 'bu-en-az-32-karakter-olmali-gizli-anahtar-1234';

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = users.find(u => u.email === email);
  if (!user || !bcrypt.compareSync(password, user.password)) {
    return res.status(401).json({ error: 'Geçersiz kimlik bilgileri' });
  }

  const token = jwt.sign(
    {
      sub: user.id,
      email: user.email,
      iat: Math.floor(Date.now() / 1000),
      'https://hasura.io/jwt/claims': {
        'x-hasura-default-role': user.role,
        'x-hasura-allowed-roles': user.allowed_roles,
        'x-hasura-user-id': user.id,
      }
    },
    JWT_SECRET,
    { expiresIn: '1h', algorithm: 'HS256' }
  );

  res.json({ token, user_id: user.id, role: user.role });
});

app.listen(3001, () => console.log('Auth servisi 3001 portunda çalışıyor'));
EOF

node index.js

Token üretildikten sonra Hasura’ya yapılan isteklerde bu token’ı doğrulamak için servisi test edelim:

# Auth servisinden token al
TOKEN=$(curl -s -X POST http://localhost:3001/login 
  -H "Content-Type: application/json" 
  -d '{"email":"[email protected]","password":"admin123"}' 
  | jq -r '.token')

echo "Token: $TOKEN"

# Token ile Hasura'ya istek at
curl -X POST http://localhost:8080/v1/graphql 
  -H "Authorization: Bearer $TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "query": "{ __typename }"
  }'

Hasura’da Permission Yapılandırması

JWT doğrulama çalışıyor, ancak izinleri de yapılandırmanız gerekiyor. Hasura Console üzerinden ya da metadata API kullanarak bu işlemi yapabilirsiniz. Metadata API yolunu tercih ediyorum çünkü bu sayede her şeyi versiyon kontrolüne alabiliyorsunuz.

# users tablosu için rol bazlı izinleri tanımla
curl -X POST http://localhost:8080/v1/metadata 
  -H "X-Hasura-Admin-Secret: myadminsecret" 
  -H "Content-Type: application/json" 
  -d '{
    "type": "pg_create_select_permission",
    "args": {
      "source": "default",
      "table": {
        "schema": "public",
        "name": "users"
      },
      "role": "user",
      "permission": {
        "columns": ["id", "email", "created_at", "profile"],
        "filter": {
          "id": {
            "_eq": "X-Hasura-User-Id"
          }
        },
        "limit": 10,
        "allow_aggregations": false
      }
    }
  }'

Bu yapılandırmayla user rolüne sahip kullanıcılar yalnızca kendi kayıtlarını görebilir. X-Hasura-User-Id session değişkeni, JWT’den otomatik olarak çekilir ve filter içinde kullanılır. Bu, Hasura’nın en güçlü özelliklerinden biridir.

RSA ile Asimetrik Anahtar Kullanımı

Production ortamlarında HS256 yerine RS256 kullanmanızı öneriyorum. Asimetrik şifreleme, private key’i yalnızca token üreten serviste tutmanıza olanak tanır. Hasura yalnızca public key’i bilmek zorundadır, bu da güvenlik açısından çok daha sağlam bir model sunar.

# RSA anahtar çifti oluştur
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

# Public key'i tek satıra çevir (Hasura için gerekli format)
PUBLIC_KEY=$(cat public.pem | tr -d 'n' | sed 's/-----BEGIN PUBLIC KEY-----/-----BEGIN PUBLIC KEY-----\n/' | sed 's/-----END PUBLIC KEY-----/\n-----END PUBLIC KEY-----/')

echo "Public Key: $PUBLIC_KEY"

# Hasura'yı RS256 ile yapılandır
# docker-compose.yml içinde HASURA_GRAPHQL_JWT_SECRET değerini güncelle:
# {"type":"RS256","key":"-----BEGIN PUBLIC KEY-----n...n-----END PUBLIC KEY-----n"}

# Private key ile token üret (Node.js örneği)
cat > generate-rs256-token.js << 'EOF'
const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('./private.pem');

const token = jwt.sign(
  {
    sub: '1',
    email: '[email protected]',
    'https://hasura.io/jwt/claims': {
      'x-hasura-default-role': 'admin',
      'x-hasura-allowed-roles': ['admin', 'user'],
      'x-hasura-user-id': '1'
    }
  },
  privateKey,
  { algorithm: 'RS256', expiresIn: '1h' }
);

console.log('RS256 Token:', token);
EOF

node generate-rs256-token.js

Token Yenileme (Refresh Token) Mekanizması

Access token’ların süresi kısa tutulmalıdır. Bu nedenle refresh token mekanizması kurmak şarttır. Aşağıdaki örnek, refresh token’ların veritabanında saklandığı tipik bir akışı göstermektedir:

# PostgreSQL'de refresh_tokens tablosu oluştur
psql -h localhost -U postgres -d hasura_db << 'EOF'
CREATE TABLE refresh_tokens (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID NOT NULL,
  token TEXT NOT NULL UNIQUE,
  expires_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  revoked BOOLEAN DEFAULT FALSE
);

CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
EOF

# Refresh token endpoint'i için auth servisine ekle
cat >> index.js << 'EOF'

const crypto = require('crypto');

// Refresh token üret ve kaydet (basitleştirilmiş)
app.post('/refresh', async (req, res) => {
  const { refresh_token } = req.body;
  
  // Veritabanında refresh token'ı kontrol et
  // Bu örnekte in-memory store kullanıyoruz
  if (!refreshTokenStore[refresh_token]) {
    return res.status(401).json({ error: 'Geçersiz refresh token' });
  }

  const userId = refreshTokenStore[refresh_token];
  const user = users.find(u => u.id === userId);

  // Yeni access token üret
  const accessToken = jwt.sign(
    {
      sub: user.id,
      'https://hasura.io/jwt/claims': {
        'x-hasura-default-role': user.role,
        'x-hasura-allowed-roles': user.allowed_roles,
        'x-hasura-user-id': user.id
      }
    },
    JWT_SECRET,
    { expiresIn: '15m' }
  );

  // Yeni refresh token üret (rotation)
  const newRefreshToken = crypto.randomBytes(64).toString('hex');
  delete refreshTokenStore[refresh_token];
  refreshTokenStore[newRefreshToken] = userId;

  res.json({ access_token: accessToken, refresh_token: newRefreshToken });
});
EOF

Webhook Fallback ile Hibrit Yaklaşım

Bazı durumlarda JWT tek başına yetmez. Örneğin token iptal etme (revocation) işlemi JWT’de doğası gereği zordur. Bu senaryolar için Hasura’nın webhook auth modunu JWT ile birlikte kullanabilirsiniz. Ancak ben genellikle kısa süreli access token ve refresh token rotation kombinasyonunu yeterli buluyorum.

Bununla birlikte, özel claim’ler üzerinden daha granüler kontrol sağlamak istediğinizde Hasura Actions devreye girer:

# Hasura Action tanımla - kullanıcı rol değişikliğini takip et
curl -X POST http://localhost:8080/v1/metadata 
  -H "X-Hasura-Admin-Secret: myadminsecret" 
  -H "Content-Type: application/json" 
  -d '{
    "type": "create_action",
    "args": {
      "name": "refreshUserClaims",
      "definition": {
        "kind": "synchronous",
        "arguments": [
          {
            "name": "user_id",
            "type": "String!"
          }
        ],
        "output_type": "RefreshClaimsOutput",
        "handler": "http://auth-service:3001/refresh-claims",
        "type": "mutation",
        "headers": []
      },
      "comment": "Kullanıcı claim bilgilerini yeniler"
    }
  }'

Production’da JWT Güvenlik Kontrol Listesi

Production ortamına geçmeden önce mutlaka kontrol etmeniz gereken noktalar var. Yıllar içinde karşılaştığım sorunların büyük çoğunluğu bu maddelerin göz ardı edilmesinden kaynaklandı.

Token süresi ve güvenlik:

  • Access token süresi: 15 dakikadan fazla tutmayın
  • Refresh token süresi: 7-30 gün arası, kullanım durumuna göre ayarlayın
  • Algoritma seçimi: Production’da RS256 veya RS512 tercih edin
  • Secret uzunluğu: HS256 için en az 256 bit (32 karakter) secret kullanın
  • HTTPS zorunluluğu: Token’lar asla HTTP üzerinden gönderilmemeli
  • Refresh token rotation: Her yenilemede eski token’ı geçersiz kılın
  • Token blacklist: Kritik operasyonlarda (logout, şifre değişikliği) token’ı Redis’te blacklist’e alın

Hasura özel yapılandırmaları:

  • HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous rolü için dikkatli izin verin
  • Admin secret: Environment variable olarak saklayın, asla kod içine yazmayın
  • Rate limiting: Auth endpoint’lerine mutlaka rate limit uygulayın
  • CORS: Yalnızca güvendiğiniz origin’lere izin verin

Monitoring ve Hata Ayıklama

JWT sorunlarını tespit etmek bazen zorlu olabilir. Hasura’nın sağladığı araçları etkin kullanmak gerekiyor:

# JWT token'ı decode et ve doğrula (debug için)
TOKEN="eyJhbGci..."

# Token payload'ını görüntüle (imzayı doğrulamadan)
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .

# Hasura'ya test isteği gönder ve header'ları görüntüle
curl -v -X POST http://localhost:8080/v1/graphql 
  -H "Authorization: Bearer $TOKEN" 
  -H "Content-Type: application/json" 
  -H "X-Hasura-Role: user" 
  -d '{
    "query": "query { users { id email } }"
  }' 2>&1 | grep -E "(< HTTP|error|errors)"

# Hasura loglarını izle
docker compose logs -f hasura | grep -E "(error|jwt|auth|permission)"

# JWT claim'lerini doğrudan test et
curl -X POST http://localhost:8080/v1/graphql 
  -H "Authorization: Bearer $TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "query": "query { __typename }"
  }' | jq '{
    result: .,
    token_info: "JWT doğrulama başarılı"
  }'

Yaygın hata mesajları ve çözümleri:

  • “Could not verify JWT”: Secret veya algoritma uyuşmazlığı, JWT_SECRET’ı kontrol edin
  • “JWT expired”: Token süresi dolmuş, refresh mekanizmasını devreye sokun
  • “x-hasura-allowed-roles doesnot contain the default role”: Token claim’lerinde default role, allowed roles içinde olmalı
  • “Permission denied”: Role bazlı izinleri Hasura Console’dan kontrol edin
  • “Missing ‘Authorization’ or ‘Cookie’ header”: Header formatını Bearer olarak doğrulayın

Gerçek Dünya Senaryosu: E-ticaret Platformu

Bir e-ticaret platformunda şu rol yapısını kullandım ve oldukça başarılı sonuçlar aldım:

  • customer: Kendi siparişlerini ve profilini görebilir, ürünleri listeleyebilir
  • vendor: Kendi ürünlerini yönetebilir, kendi mağazasının siparişlerini görebilir
  • support: Tüm siparişleri ve müşteri bilgilerini okuyabilir, düzenleyemez
  • admin: Tam erişim
  • anonymous: Yalnızca ürün listeleme ve detay görüntüleme

Bu yapıda JWT claim’leri şöyle görünüyordu:

# Vendor için örnek JWT payload
cat << 'EOF'
{
  "sub": "vendor-uuid-123",
  "email": "[email protected]",
  "iat": 1699900000,
  "exp": 1699900900,
  "https://hasura.io/jwt/claims": {
    "x-hasura-default-role": "vendor",
    "x-hasura-allowed-roles": ["vendor", "customer", "anonymous"],
    "x-hasura-user-id": "vendor-uuid-123",
    "x-hasura-store-id": "store-uuid-456",
    "x-hasura-subscription-tier": "premium"
  }
}
EOF

# x-hasura-store-id claim'ini Hasura permission'da kullan
# Vendor yalnızca kendi mağazasının ürünlerini yönetebilir:
# filter: { store_id: { _eq: "X-Hasura-Store-Id" } }

Bu yaklaşımda x-hasura-store-id gibi özel claim’ler sayesinde veritabanı sorgu katmanında otomatik filtreleme yapabiliyorsunuz. Uygulama kodunda manuel kontrol yazmanıza gerek kalmıyor, Hasura tüm bunları otomatik hallediyor.

Sonuç

Hasura ile JWT kimlik doğrulama entegrasyonu, doğru yapılandırıldığında son derece güçlü ve ölçeklenebilir bir güvenlik modeli sunar. Bu yazıda ele aldığımız başlıkları özetleyecek olursak: HS256 ile başlayıp production’da RS256’ya geçmek, kısa süreli access token ve rotation’lı refresh token kullanmak, Hasura’nın session variable mekanizmasından yararlanarak uygulama katmanında tekrarlayan güvenlik kontrollerinden kaçınmak ve token claim’lerini veritabanı filtreleriyle entegre etmek temel prensiplerdir.

En çok dikkat edilmesi gereken nokta şu: Hasura’nın permission sistemi ne kadar sağlam kurulursa, uygulama katmanınız o kadar temiz kalır. İzin mantığını GraphQL şemasına gömmek yerine Hasura metadata’sında merkezi olarak yönetmek, uzun vadede hem bakımı kolaylaştırır hem de güvenlik açıklarını minimize eder. Token süreleri, algoritma seçimi ve refresh mekanizması üçlüsünü doğru kurduğunuzda sistem hem güvenli hem de kullanıcı dostu bir deneyim sunar.

Bir yanıt yazın

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