JWT Authentication: Security Best Practices in 2026

Secure your APIs with JWT best practices: RS256 vs HS256, HttpOnly cookies, token rotation, refresh patterns, and common vulnerabilities to avoid in 2026.

E
ECOSIRE Research and Development Team
|19 Mart 202610 dk okuma2.1k Kelime|

JWT Kimlik Doğrulaması: 2026'nın En İyi Güvenlik Uygulamaları

JSON Web Token'ları her yerdedir ancak çoğu uygulamada en az bir kritik güvenlik açığı bulunur. Saldırı yüzeyi göründüğünden daha büyüktür: algoritma karışıklığı saldırıları, XSS aracılığıyla token hırsızlığı, eksik süre sonu doğrulaması ve uygunsuz gizli yönetim, üretim sistemlerinde bulunan en yaygın güvenlik açıkları arasındadır. JWT'yi doğru yapmak, bir kütüphaneyi arayıp devam etmek meselesi değildir; her katmanda bilinçli kararlar almayı gerektirir.

Bu kılavuz, imzalama algoritması seçimi ve belirteç yapısından depolama, döndürme, iptal etme ve gerçek dünyadaki NestJS uygulama modellerine kadar JWT güvenlik yaşam döngüsünün tamamını kapsar; böylece saldırılara karşı gerçekten dayanıklı kimlik doğrulaması oluşturabilirsiniz.

Önemli Çıkarımlar

  • Dağıtılmış sistemler için daima RS256 (asimetrik) kullanın; HS256 yalnızca API sunucusunun hem veren hem de doğrulayıcı olması durumunda
  • Belirteçleri HttpOnly, Secure, SameSite=Lax çerezlerinde saklayın; hiçbir zaman localStorage veya sessionStorage'da saklayın
  • Her zaman exp, iss, aud ve alg taleplerini doğrulayın — imzasız none algoritmasına asla güvenmeyin
  • Yenileme jetonu rotasyonunu uygulayın: her yenileme yeni bir çift üretir ve eski yenileme jetonunu geçersiz kılar
  • Erişim belirtecinin TTL'sini kısa tutun (15 dakika); veritabanında depolanan opak yenileme belirteçlerini kullan
  • Hassas verileri (şifreler, SSN'ler, ödeme bilgileri) asla JWT veri yüklerinde saklamayın — veriler base64'tür, şifrelenmemiştir
  • Redis red listesi veya veritabanındaki sürüm sayacı aracılığıyla belirteç iptalini uygulayın
  • Güvenlik denetim izleri için tüm jeton verme ve yenileme olaylarını günlüğe kaydedin

JWT Yapısı ve Talepler

Bir JWT'nin üç bölümü vardır: noktalarla ayrılmış ve base64url kodlu başlık, yük ve imza.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0MjM0NTYwMCwiZXhwIjoxNzQyMzQ2NTAwLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIn0.
[signature]

Kodu çözülmüş yük:

{
  "sub": "user_123",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1742345600,
  "exp": 1742346500,
  "iss": "https://api.example.com",
  "aud": "https://app.example.com"
}

Üretim için gerekli talepler:

  • sub — benzersiz kullanıcı tanımlayıcı (asla tek başına e-posta göndermeyin — e-postalar değişir)
  • exp — geçerlilik bitiş zaman damgası (her zaman gerekli)
  • iat — zaman damgasında yayınlandı (saat çarpıklığını tespit et)
  • iss — veren kuruluş URL'si (beklenen veren kuruluşa göre doğrulayın)
  • aud — hedef kitle (hizmetlerde belirtecin yeniden kullanılmasını önlemek için doğrulama)
  • jti — JWT Kimliği (belirteç başına benzersizdir, tam iptali sağlar)

RS256 vs HS256: Hangi Algoritmanın Kullanılacağı

Bu, JWT yapılandırmasındaki en etkili güvenlik kararıdır.

HS256 (HMAC-SHA256) — Simetrik

// Both signing and verifying require the same secret
const token = jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256' });
const verified = jwt.verify(token, process.env.JWT_SECRET);

Yalnızca şu durumlarda kullanın: Belirteçleri imzalayan hizmet, bunları doğrulayan hizmetle aynıdır. HS256, monolitik API'ler için uygundur ancak mikro hizmetlerde tehlikelidir; belirteçleri doğrulayabilen herhangi bir hizmet, bunları da oluşturabilir.

RS256 (RSA-SHA256) — Asimetrik

// Sign with private key (only the auth server holds this)
const privateKey = fs.readFileSync('/secrets/jwt-private.pem');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256', keyid: 'key-2026-01' });

// Verify with public key (any service can do this safely)
const publicKey = fs.readFileSync('/secrets/jwt-public.pem');
const verified = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // NEVER omit this — prevents algorithm confusion
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
});

Bir üretim RSA anahtar çifti oluşturun:

# Generate 4096-bit RSA private key
openssl genrsa -out jwt-private.pem 4096

# Extract public key
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem

Anahtar döndürme: Başlıkta keyid (kid) kullanın. Geçerli ortak anahtarları /.well-known/jwks.json adresinde yayınlayın. Hizmetler JWKS'yi önbelleğe alır ve bilinmeyen kid'yi getirir.


HttpOnly Çerez Depolama

Bu tartışılamaz. Belirteçleri localStorage veya sessionStorage'de saklamak, XSS saldırılarından enjekte edilen komut dosyaları da dahil olmak üzere sayfada çalışan tüm JavaScript'ler tarafından erişilebilir olmasını sağlar.

// NestJS: set HttpOnly cookie after authentication
@Post('login')
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
  const { accessToken, refreshToken } = await this.authService.login(dto);

  const cookieBase = {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax' as const,
    path: '/',
    domain: process.env.COOKIE_DOMAIN, // '.example.com' for subdomain sharing
  };

  res.cookie('access_token', accessToken, {
    ...cookieBase,
    maxAge: 15 * 60 * 1000, // 15 minutes
  });

  res.cookie('refresh_token', refreshToken, {
    ...cookieBase,
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
    path: '/auth/refresh', // Scope refresh token to only the refresh endpoint
  });

  return { message: 'Login successful' };
}
// Extract token from cookie in JWT strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (req) => req?.cookies?.access_token, // Cookie first
        ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer fallback for API clients
      ]),
      secretOrKey: configService.get('JWT_PUBLIC_KEY'),
      algorithms: ['RS256'],
      issuer: configService.get('JWT_ISSUER'),
      audience: configService.get('JWT_AUDIENCE'),
    });
  }

  async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
    // Always check token version against database
    const user = await this.usersService.findById(payload.sub);
    if (!user || user.tokenVersion !== payload.tokenVersion) {
      throw new UnauthorizedException('Token invalidated');
    }
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

Jeton Rotasyonunu Yenile

Asla uzun ömürlü bir erişim belirteci vermeyin. Bunun yerine, erişim belirteçlerini kısa ömürlü tutun ve her kullanımda yenileme belirteçlerini dönüşümlü olarak kullanın.

// auth.service.ts
@Injectable()
export class AuthService {
  async refreshTokens(refreshToken: string, ipAddress: string) {
    // 1. Look up the refresh token in the database
    const storedToken = await this.db.query.refreshTokens.findFirst({
      where: and(
        eq(refreshTokens.token, this.hashToken(refreshToken)),
        eq(refreshTokens.revoked, false),
        gt(refreshTokens.expiresAt, new Date())
      ),
      with: { user: true },
    });

    if (!storedToken) {
      // Possible reuse attack — revoke all tokens for this user
      if (storedToken?.userId) {
        await this.revokeAllUserTokens(storedToken.userId);
        await this.alertService.send({
          message: `Refresh token reuse detected for user ${storedToken.userId}`,
          severity: 'high',
        });
      }
      throw new UnauthorizedException('Invalid refresh token');
    }

    // 2. Revoke the used refresh token (rotation)
    await this.db
      .update(refreshTokens)
      .set({ revoked: true, revokedAt: new Date(), revokedByIp: ipAddress })
      .where(eq(refreshTokens.id, storedToken.id));

    // 3. Issue new token pair
    const newAccessToken = this.issueAccessToken(storedToken.user);
    const newRefreshToken = await this.issueRefreshToken(
      storedToken.user.id,
      ipAddress,
      storedToken.family // Track token families for reuse detection
    );

    return { accessToken: newAccessToken, refreshToken: newRefreshToken };
  }

  private hashToken(token: string): string {
    return crypto.createHash('sha256').update(token).digest('hex');
  }
}

Yenileme belirteçleri için veritabanı şeması:

CREATE TABLE refresh_tokens (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token       VARCHAR(64) NOT NULL UNIQUE, -- SHA256 hash of the actual token
  family      UUID NOT NULL,               -- Token family for reuse detection
  expires_at  TIMESTAMPTZ NOT NULL,
  revoked     BOOLEAN DEFAULT false,
  revoked_at  TIMESTAMPTZ,
  revoked_by_ip INET,
  created_at  TIMESTAMPTZ DEFAULT now(),
  created_by_ip INET
);

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

Token İptal Stratejileri

JWT'ler tasarım gereği vatansızdır; bir kez yayınlandıktan sonra ek altyapı olmadan onları "geri alamazsınız". İşte ödünleşime göre sıralanan üç yaklaşım:

1. Kısa TTL (15 dakika)

En basit iptal stratejisi: Erişim belirteçlerinin geçerliliği, nadiren gerekli olacak kadar hızlı bir şekilde sona erer. Veritabanındaki yenileme jetonunun anında iptal edilmesiyle eşleştirin.

2. Token Versiyon Sayacı

Kullanıcılar tablosunda bir tokenVersion saklayın. Mevcut tüm belirteçleri geçersiz kılmak için bunu artırın:

// Increment version to logout all sessions
await this.db
  .update(users)
  .set({ tokenVersion: sql`token_version + 1` })
  .where(eq(users.id, userId));

// In JWT payload
const payload = {
  sub: user.id,
  tokenVersion: user.tokenVersion, // embedded at sign time
};

// In JwtStrategy.validate()
if (user.tokenVersion !== payload.tokenVersion) {
  throw new UnauthorizedException();
}

İstek başına bir veritabanı araması gerektirir — çoğu uygulama için kabul edilebilir.

3. Redis Red Listesi

Veritabanı araması gerekmeden anında iptal için:

// Revoke specific token by JTI
async revokeToken(jti: string, expiresIn: number): Promise<void> {
  const key = `token:revoked:${jti}`;
  await this.redis.set(key, '1', 'EX', expiresIn);
}

// Check in JWT strategy before accepting
async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
  const revoked = await this.redis.get(`token:revoked:${payload.jti}`);
  if (revoked) {
    throw new UnauthorizedException('Token revoked');
  }
  return this.buildUser(payload);
}

Değişim: İstek başına bir Redis araması, ancak Redis O(1)'dir ve milisaniyenin altındadır. Yüksek güvenlikli uç noktalar için kabul edilebilir.


Yaygın Güvenlik Açıkları ve Azaltıcı Etkiler

Algoritma Karışıklık Saldırısı

alg: "none" saldırısı: Saldırgan imzayı soyar ve alg'yi none olarak ayarlar, ardından tahrif edilmiş bir veri gönderir. İmzasız belirteçleri kabul eden kütüphaneler her türlü yükü kabul edecektir.

// WRONG — never do this
jwt.verify(token, secret); // Accepts alg:none if library allows it

// CORRECT — always specify algorithms explicitly
jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Whitelist only what you use
});

JWT Başlık Ekleme (jwk/jku)

Saldırganlar, kendi anahtar sunucularına işaret eden jku (JWKS URL) veya jwk (satır içi anahtar) başlığına sahip bir JWT oluşturur ve ardından kendi özel anahtarlarıyla imzalar. Savunmasız bir doğrulayıcı, saldırganın anahtarlarını alır ve jetonu kabul eder.

// WRONG — never fetch keys from the token header
const jwksUri = decodedHeader.jku; // Attacker-controlled!

// CORRECT — always use a pinned, config-driven JWKS URI
const jwksClient = createRemoteJWKSet(new URL(configService.get('JWKS_URI')));

HS256'nın Zayıf Sırları

HS256 için 32 karakterlik bir ASCII sırrı yaklaşık 190 bitlik entropiye sahiptir; bu yetersizdir. Kriptografik olarak güvenli rastgele bir kaynaktan en az 256 bit kullanın:

# Generate a strong HS256 secret
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"

NestJS JWT Modül Yapılandırması

// auth.module.ts
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        privateKey: config.get('JWT_PRIVATE_KEY'),
        publicKey: config.get('JWT_PUBLIC_KEY'),
        signOptions: {
          algorithm: 'RS256',
          expiresIn: '15m',
          issuer: config.get('JWT_ISSUER'),
          audience: config.get('JWT_AUDIENCE'),
        },
        verifyOptions: {
          algorithms: ['RS256'],
          issuer: config.get('JWT_ISSUER'),
          audience: config.get('JWT_AUDIENCE'),
        },
      }),
    }),
  ],
})
export class AuthModule {}

Genel Anahtar Dağıtımı için JWKS Uç Noktası

// jwks.controller.ts
@Controller('.well-known')
export class JwksController {
  @Get('jwks.json')
  @Public()
  async getJwks() {
    const publicKeyPem = this.configService.get('JWT_PUBLIC_KEY');
    // Convert PEM to JWK format using the 'jose' library
    const publicKey = await importSPKI(publicKeyPem, 'RS256');
    const jwk = await exportJWK(publicKey);

    return {
      keys: [
        {
          ...jwk,
          use: 'sig',
          alg: 'RS256',
          kid: this.configService.get('JWT_KEY_ID'), // e.g., 'key-2026-01'
        },
      ],
    };
  }
}

Bu uç noktayı agresif bir şekilde önbelleğe alın; genel anahtarlar nadiren değişir. Cache-Control: public, max-age=3600 değerini ayarlayın.


Güvenlik Kontrol Listesi

JWT kimlik doğrulamasını üretime dağıtmadan önce şunları doğrulayın:

  • Algoritma, hem imzalama hem de doğrulama çağrılarında açıkça RS256 veya HS256 olarak ayarlandı
  • exp, iss, aud her istekte doğrulanır
  • Erişim jetonu TTL ≤ 15 dakika
  • HttpOnly, Secure, SameSite=Lax çerezlerinde saklanan jetonlar
  • Jeton rotasyonunu yenileme uygulandı — eski jeton her kullanımda iptal edilir
  • Veritabanında SHA256 karma değeri olarak depolanan belirteci yenileyin (düz metin değil)
  • Hedeflenen iptal özelliği için jti talebi eklendi
  • Redis'in reddedilenler listesi yüksek güvenlikli uç noktalar açısından kontrol edildi
  • Dağıtılmış hizmetler için JWKS uç noktası yayınlandı
  • Gizli anahtarlar yöneticisinde (AWS Secrets Manager, Vault) saklanan özel anahtarlar
  • Anahtar döndürme prosedürü belgelendi ve test edildi
  • Tüm jeton verme ve geçersiz kılma olayları günlüğe kaydedildi

Sıkça Sorulan Sorular

İmzayı doğrulamadan bir JWT'nin kodunu çözmek güvenli midir?

Doğrulama olmadan kod çözme, veriyi okumak için güvenlidir ancak yetkilendirme için asla içeriğe güvenmeyin. Taleplere ilişkin harekete geçmeden önce daima imzayı doğrulayın. Çoğu kitaplıktaki jwt.decode() işlevi doğrulamayı atlar; bunu yalnızca tanılama için veya doğrulama için doğru anahtarı seçmeden önce başlıktan kid okumak için kullanın.

Tarayıcı uygulamaları için çerezleri mi yoksa Yetkilendirme başlıklarını mı kullanmalıyım?

Tarayıcı uygulamaları için HttpOnly çerezleri, yerel mobil uygulamalar için Yetkilendirme başlıkları ve sunucudan sunucuya API çağrıları. Çerezler XSS sızmasına karşı bağışıklıdır (JavaScript, HttpOnly çerezlerini okuyamaz). Mobil uygulamalar, çerezleri etkili bir şekilde kullanamaz ve cihazın güvenli anahtar deposunda saklanan Bearer token'larını kullanamaz.

Ön uçta belirtecin süresinin dolmasını nasıl hallederim?

401 yanıtlarını durdurun ve orijinal isteği yeniden denemeden önce sessiz yenilemeyi deneyin. React'ta bir Axios kullanın veya önleyiciyi getirin. Yenileme de başarısız olursa (süresi dolmuşsa veya iptal edilmişse), oturum açma sayfasına yönlendirin. Paralel yenileme fırtınalarını önlemek için uçuş sırasında tek bir yenileme sözünü tutun.

Erişim belirteçleri ile yenileme belirteçleri arasındaki fark nedir?

Erişim belirteçleri kısa ömürlüdür (15 dakika), durum bilgisi yoktur ve her API isteğinde ortak anahtar veya paylaşılan sır ile doğrulanır. Yenileme belirteçleri uzun ömürlüdür (7-30 gün), opaktır (JWT'ler değil rastgele dizeler) ve veritabanında sunucu tarafında depolanır. Yenileme belirteci uç noktası, yenileme belirteçlerinin kullanıldığı tek yerdir; bunları dar bir çerez yolu ile kapsamlayın.

Kullanıcı rollerini JWT verisinde saklayabilir miyim?

Evet, ancak belirteçte kodlanan rollerin, belirtecin süresi dolana kadar önbelleğe alındığını unutmayın. Bir kullanıcının yönetici rolünü iptal ederseniz, mevcut erişim belirtecinin süresi dolana kadar (15 dakikaya kadar) bu rolü korur. Yüksek güvenlikli rol değişiklikleri için kullanıcıyı Redis reddedilenler listesine de ekleyin veya anında yeniden kimlik doğrulamayı zorunlu kılmak için belirteç sürümünü artırın.

JWT'lerle "beni hatırla" özelliğini nasıl uygularım?

Kullanıcı "beni hatırla" seçeneğini işaretlediğinde standart 30 güne kıyasla daha uzun ömürlü bir yenileme jetonu (90 gün) yayınlayın. Kalıcı oturumları kullanıcının güvenlik ayarlarında ayrı ayrı görüntüleyebilmeniz ve iptal edebilmeniz için yenileme belirteci veritabanı satırına bir persistent bayrağı depolayın. Hiçbir zaman erişim belirteci TTL'yi genişletmeyin; bu, amacı boşa çıkarır.


Sonraki Adımlar

JWT kimlik doğrulamasının doğru yapılması her güvenli web uygulamasının temelidir. RS256 imzalamadan HttpOnly çerez depolamaya, yenileme jetonu rotasyonundan ve iptal stratejilerine kadar bu kılavuzdaki modeller kullanıcılarınızı en yaygın kimlik doğrulama saldırılarından korur.

ECOSIRE, tüm arka uç projelerimizde Authentik ile OIDC entegrasyonu, HttpOnly çerez akışları ve Redis destekli belirteç yönetimi de dahil olmak üzere, savaşta test edilmiş kimlik doğrulama mimarisini uygular. Kimlik doğrulama katmanınızı nasıl güçlendirebileceğimizi öğrenmek için Güvenlik odaklı geliştirme hizmetlerimizi keşfedin.

E

Yazan

ECOSIRE Research and Development Team

ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.

WhatsApp'ta Sohbet Et