JWT Authentication

Bu makalede modern web uygulamalarının göz bebeği olan JWT authentication yönteminden bahsediyor olacağım. JWT (JSON Web Token) bilgi alışverişi ve kimlik doğrulaması yapmak için kullanılan bir token türüdür.

Header, Payload, Signature olmak üzere 3 bölümden/bileşenden oluşur. Bu bileşenler birbirinden nokta ile ayrılır.

header.payload.signature

Header; token türü ve kullanılan imzalama algoritmasını belirtir.
Payload; Token içerisinde taşınan datadır.
Signature: Token'ın bütünlüğünü ve doğruluğunu sağlamak için kullanılan imzadır.

JWT Authentication Avantajları Nelerdir?

Durumsuz Yapı: Sunucunun, kullanıcı oturumlarını saklamak için bir duruma ihtiyaç duymaması ve sunucuya yük bindirmemesi.

Taşınabilirlik: JWT, bir URL parametresi olarak, HTTP başlığında veya HTML5 web storage'da saklanılarak kullanılabilir.

Performans: Sunucunun oturum bilgilerini saklaması gerekmediği için bir yük bindirmez.

Kolaylık: JSON formatında olduğu için okunurluğu ve kullanımı oldukça kolaydır.

JWT Authentication Dezavantajları Nelerdir?

Güvenlik Riskleri : Ele geçirilirse süresi dolana kadar kötü amaçlı kullanılabilir.

Token Yenileme Zorluğu: Süresi dolduğunda yenilenmesi gerekir. Bu da kullanıcı deneyimini olumsuz yönde etkileme riskini doğurur.

Boyut: JWT sakladığı datanın büyüklüğüne bağlı olarak büyüyecektir. Bu da ağ trafiğinin artmasına sebep olabilir.

Saklama Sorunları: Tarayıcıda saklanan JWT'ler, XSS saldırılarına karşı savunmasız olabilir.

Güvenlik Risklerini Azaltmak İçin Neler Yapılabilir?

Bir JWT kullanarak kullanıcılarınızın kimlik doğrulamasını yapacaksanız riskleri azaltmak için uygulamanızın önemli olduğu bazı şeyler var.

Short-lived Tokens: Token yaşam süresini mümkün olduğunca kısa tutun. Projenize göre bu süreyi 15 dakika ile 1 saat arasında tutmak iyi bir seçimdir.

Refresh Tokens: Kullanıcınızın yeni bir token almak için kullanabileceği bir token kullanın. Bu tokenların ömürleri daha uzun tutulabilir.

HTTP Only ve Secure Cookie; JWT'yi tarayıcıda saklarken localStorage ya da sessionStorage yerine HttpOnly ve Secure özelliklerine sahip bir çerezde saklayın.

XSS: Uygulamanızı XSS saldırılarına karşı koruyun. Bunun yolu içerik güvenliği politikalarına (Content Security Policies) uygun uygulama geliştirmekten geçer.

İmzalama algoritması seçimini doğru yapın. HS256 ya da RS256 gibi güçlü imzalama algoritmaları ile JWT oluşturun.

Anahtar güvenliğini sağlayın. İmzalamak için kullandığınız anahtarın güvenli bir şekilde saklandığına emin olun. Production ve development ortamları için farklı anahtarlar kullanın.

Hassas bilgileri şifreleyerek (encryption) ek bir güvenlik katmanı sağlayın.

Token Blacklist oluşturun. Ele geçirilen veya iptal edilen tokenları takip etmek için bir kara liste mekanizması oluşturun.

Token iptal mekanizması kurun. Kullanıcı oturumu kapattığında veya şüpheli aktiviteler algılandığında o kullanıcının tüm tokenlarını iptal edebileceğiniz bir mekanizma oluşturun.

Kullanıcının sadece yetkili olduğu kaynaklara erişimi sağladığını doğrulamak için her istekte yetkilendirmeyi kontrol edin.

Yüksek güvenlik gerektiren işlemler için 2FA (iki faktörlü kimlik doğrulama) kullanmayı düşünün.

JWT Oluşturma ve Kullanımı

Bir JWT nasıl oluşturulur ve JWT ile kimlik doğrulaması nasıl yapılır en yalın hali ile şöyle yapılır. (PHP ile)

Konuyu daha iyi anlayabilmeniz adına ben encode ve decode işlemlerini kendim yönettiğim basit bir sınıf yazdım. Siz bu işlem için kesinlikle "firebase/php-jwt" paketini kullanın.

class JWT
{

protected const ALGORITHM = 'HS256';

protected const SIGNATURE_ALGORITHM = 'sha256';

public static function encode(string $key, array $payload, ?int $ttl = null): string
{
$header = base64_encode(json_encode([
'typ' => 'JWT',
'alg' => self::ALGORITHM,
]));
$payload = base64_encode(json_encode([
'data' => $payload,
'created_at' => time(),
'ttl' => $ttl ?? null,
]));

return $header . "." . $payload . "." . self::signature($key, $header, $payload);
}

public static function decode(string $key, string $jwt): array|false
{
try {
$split = explode('.', $jwt);
$signature = $split[2];

if ($signature != self::signature($key, $split[0], $split[1])) {
return false;
}
$payload = json_decode(base64_decode($split[1]), true);

if ($payload['ttl'] !== null && time() - $payload['created_at'] > $payload['ttl']) {
return false;
}

return $payload['data'];
} catch (\Throwable $e) {
return false;
}
}

private static function signature(string $key, string $header, string $payload): string
{
return base64_encode(hash_hmac(self::SIGNATURE_ALGORITHM, $header . "." . $payload, $key, true));
}

}

$secretKey = 'Çok Gizli Anahtar';
$payload = [
'user_id' => 1234,
'username' => 'muhammetsafak',
];

$jwt = JWT::encode($secretKey, $payload, 3600);

echo "Sonraki gelişiniz de bu token'ı kullanın : " . $jwt;

$headers = apache_request_headers();
if (!isset($headers['Authorization']) || !str_starts_with($headers['Authorization'], 'Bearer ')) {
echo "JWT Authorization başlığında Bearer token olarak gönderin.";
}

$secretKey = 'Çok Gizli Anahtar';
$jwt = explode(' ', $headers['Authorization'], 2)[1];

$payload = JWT::decode($secretKey, $jwt);
if (!$payload) {
die("Yetkisiz erişim");
}

echo "Hoş geldin @" . $payload['username'];

Yukarıdaki örnekte kullanıcıya 1 saat geçerli olacak bir JWT oluşturduk ve sonrasında bu tokenı Authorization başlığında bir Bearer token olarak göndermesini isteyerek authentication sürecini tamamlamış olduk. İşin arka plandaki çalışma prensibi bundan ibaret...