Skip to content
Bloga Dönün
Güvenlik

JWT invalid signature hatası: tüm nedenleri ve çözümleri

JWT imza doğrulama hatasının (invalid signature) nedenleri: dile göre değişen anahtar baytları, .env sonundaki satır sonu, uyumsuz algoritma. Ücretsiz çözücü.

14 dakika okuma

JWT invalid signature hatası: tüm nedenleri ve çözümleri

Bir JWT’de invalid signature hatası tam olarak tek bir şey anlamına gelir: doğrulayıcınızın hesapladığı imza, token’ın taşıdığı imzaya eşit değildir. Mesajın tamamı bu. Token’ın süresinin dolduğu, kullanıcının yetkisinin eksik olduğu ya da JWT kütüphanenizin bozuk olduğu anlamına gelmez. HMAC’e giren baytlarda ya da doğrulama çağrısına giren genel anahtarda, imzalayan tarafla denetleyen taraf arasında bir fark vardır.

Vakaların çoğunda suçlu token değil, anahtar malzemesidir. Nereden başlayacağınıza şuradan karar verin:

Başlıkta hangi algoritma yazıyor?
├─ HS256 / HS384 / HS512  → neredeyse her zaman gizli anahtar sorunu
│    ├─ imzalayan ve doğrulayan farklı dillerde mi?     → Bölüm 3
│    └─ aynı dil, yerelde çalışıyor, prod'da patlıyor?  → Bölüm 4
└─ RS256 / ES256 / PS256  → neredeyse her zaman anahtar biçimi ya da yanlış anahtar
     └─ → Bölüm 7

Token bir gateway'den, proxy'den ya da kopyala-yapıştırdan mı geçti? → Bölüm 6
Hata yalnızca birkaç saat sonra ya da tek bir makinede mi çıkıyor?  → Bölüm 8

Aşağıdaki her bölüm, çalıştırabileceğiniz bir şeyle bitiyor. En hızlı ilk hamle için token’ı JWT çözücü aracına yapıştırın ve alg alanını okuyun; yukarıdaki dalların yarısı siz bunu öğrenir öğrenmez kapanır.

1. invalid signature ne demek

Farklı kütüphaneler aynı hata için farklı metinler basar. Doğru rehberde olduğunuzdan emin olmak için kendinizinkini bu listede bulun:

  • Node jsonwebtoken: JsonWebTokenError: invalid signature
  • Python PyJWT: InvalidSignatureError: Signature verification failed
  • Java jjwt: SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

Üçü de aynı noktada, aynı kod yolunda tetiklenir. Kütüphane token’ınızın ilk iki parçasını alır, ona verdiğiniz anahtarla imzayı yeniden hesaplar ve sonucu üçüncü parçayla bayt bayt karşılaştırır. Eşit değilse hata fırlatır.

Karşılaştırma birebirdir ve iki değerin ne kadar farklı olduğuna dair hiçbir bilgi taşımaz. Gizli anahtardaki tek baytlık bir fark ile tümüyle yanlış bir anahtar, birebir aynı hata mesajını üretir. Bu yüzden rehberin geri kalanı hatayı daha dikkatli okumakla değil, girdi uzayını daraltmakla ilgileniyor.

Bu hata tetiklendiğinde henüz neyin olmadığına dikkat edin. Claim doğrulaması imza doğrulamasından sonra çalışır; yani exp, nbf, aud ve iss daha hiç incelenmemiştir. JWT imza doğrulamanız başarısız olduysa token’ın içeriği tanı açısından ilgisizdir — yine de okunabilir, çünkü bir JWT şifrelenmiş değil kodlanmıştır. Başlığı ve yükü çözmek hiçbir anahtar gerektirmez; parça parça anlatımı isterseniz JWT nasıl çözülür yazısına bakın.

Başlıktaki iki alan bundan sonra nereye gideceğinizi belirler: alg paylaşılan bir gizli anahtarın mı yoksa bir anahtar çiftinin mi peşinde olduğunuzu, kid ise imzalayanın hangi anahtarı kullandığını sandığını söyler.

2. İmza, nesnenizi değil kodlanmış dizeyi kapsar

Çoğu geliştiricinin ters bildiği yer burası. JSON Web Signature spesifikasyonu RFC 7515, JWS imzalama girdisini şu ASCII dizesi olarak tanımlar:

BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload)

HMAC o dize üzerinden hesaplanır; claims map’iniz ya da dilinizin yapılandırılmış veri saydığı başka bir şey üzerinden değil. Bu yazı boyunca kullanılan imzalama girdisi, standart örnek yükten alınmıştır:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

Bunun bir sonucu var ve ekipleri sürekli yakalıyor: yükü çözüp yeniden kodlayan her katman imzayı yok eder. JSON serileştirmesi kanonik değildir. Bir map çoğu dilde gidip geldiğinde anahtar sırası değişir. Boşluk ortaya çıkar ya da kaybolur. ASCII dışı karakterleri bir serileştirici \uXXXX olarak kaçırır, bir diğeri olduğu gibi basar. Sayılar yeniden biçimlenir, 1516239022 geri dönerken 1516239022.0 olabilir. Bunların her biri farklı bir base64url dizesi, dolayısıyla farklı bir imzalama girdisi, dolayısıyla farklı bir imza üretir.

Bunu pratikte tetikleyen bileşenler:

  • JWT’yi bir tenant ID ile zenginleştirmek için ayrıştırıp token’ı yeniden yayan bir API gateway’i.
  • Başlıkları “normalleştirip” Authorization değerini yeniden yazan bir günlükleme ya da izleme ara katmanı.
  • Okumak için bir token’ı güzel biçimlendirip sonra o güzel hâlini geri yapıştıran bir geliştirici.

İmzalayanınız ile doğrulayıcınız arasındaki herhangi bir bileşen token’ı yeniden yazabiliyorsa ilk şüpheli o bileşendir. Token’lar taşıma sırasında opak dizelerdir; güvenli olan tek işlemler saklamak, kopyalamak ve karşılaştırmaktır.

3. Aynı gizli anahtar, farklı baytlar

“Gizli anahtar harfi harfine aynı, diff de aldım” diye başlayan hata kayıtlarının çoğu buraya çıkar.

HMAC bir dize tüketmez. Bayt tüketir. Yapılandırma dosyanız, gizli anahtar yöneticiniz ve ortam değişkenleriniz dize saklar. Birinin birini diğerine çevirmesi gerekir ve bu dönüşüm JWT kütüphaneleri arasında standartlaşmış değildir. İki servis karakteri karakterine aynı gizli anahtarı tutup yine de farklı imzalar hesaplayabilir.

Aşağıdaki iki imzayı Bölüm 2’deki imzalama girdisiyle yerelde hesapladık. Gizli anahtar dizesi 36 karakter:

c2VjcmV0LWtleS0xMjM0NTY3ODkwYWJjZGVm
Bayt yorumuBaytAnahtar aslında nedirOrtaya çıkan HS256 imzası
UTF-8 metni sayıldığında36görünen 36 karakterin kendisitUQobLFxHSIQqURPqGT59pkBnqQ95sZ0-JC1hE_4Zak
Önce base64 çözüldüğünde27secret-key-1234567890abcdef53ISDuciq-ov8YF1Ezwy6zo6KUO-1tpwz2oW7gVPuIM

Aynı gizli anahtar dizesi, aynı algoritma, aynı yük; ortak hiçbir yanı olmayan iki imza. Hangi taraf “yanlış” yaptıysa invalid signature bildirir ve yapılandırma dosyasını ne kadar diff’lerseniz diff’leyin hiçbir şey çıkmaz, çünkü yapılandırma dosyaları birbirinin aynısıdır.

UTF-8 okumasına karşılık gelen tam token, yeniden üretmek isterseniz:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.tUQobLFxHSIQqURPqGT59pkBnqQ95sZ0-JC1hE_4Zak

Bunu yukarıdaki gizli anahtarla JWT çözücü aracına yapıştırın, doğrulanır. Gizli anahtarı önce base64 olarak çözerseniz doğrulanmaz.

Her kütüphane bir dizeyi anahtar baytlarına nasıl çevirir

Belgelenmiş davranışa bağlı kalın. Aşağıdaki tabloda son sütun ilkinden daha önemli.

Runtime / kütüphaneDizeden bayta davranışKararı kim verir
Node jsonwebtokendizenin UTF-8 baytlarıkütüphane
Python PyJWTdizenin UTF-8 baytlarıkütüphane
Java jjwt, eski String aşırı yüklemesiplatformun base64 codec’i, bkz. jwtk/jjwt#204kütüphane
Go golang-jwtdoğrudan []byte alırsiz, çağrı yerinde
.NETdoğrudan byte[] alırsiz, çağrı yerinde

Diller arası uyuşmazlıkların tarihsel kaynağı Java satırıdır ve ayrıntısını doğru vermek gerekiyor. Eski jjwt sürümlerinde signWith(SignatureAlgorithm, String) ve kardeşleri String’i ham baytlarına çevirmek yerine bir base64 codec’inden geçiriyordu; byte[] aşırı yüklemeleri ise baytları verildiği gibi kullanıyordu. Tek bir gizli anahtarı paylaşan bir Node servisi ile bir Java servisi bu yüzden anlaşamıyordu. O String API’si jjwt 0.10’dan beri kullanımdan kaldırıldı ve güncel biçim açık:

SecretKey key = Keys.hmacShaKeyFor(secretBytes);

Bu, “Java JWT’leri böyle yapar” demek değil. Tek bir kütüphanenin eski aşırı yüklemesi söz konusu ve byte[] geçiren güncel jjwt kodunda hiçbir belirsizlik yok. Node tarafındaki karşılığı auth0/node-jsonwebtoken#208 kaydı; orada Java’da imzalanan token’lar Node’da doğrulanmıyordu. PHP’nin firebase/php-jwt kütüphanesine karşı da benzer kayıtlar var (bkz. firebase/php-jwt#153), ama o kütüphanenin bayt işleyişini kendimiz doğrulamadık; bunu bir tanı değil, bir ipucu sayın.

Go ve .NET ayrı bir kategoride. İki kütüphane de sizin adınıza karar vermez; []byte / byte[] parametresini size uzatıp geri çekilir. []byte(secret) ve Encoding.UTF8.GetBytes(secret) UTF-8 verir, Convert.FromBase64String(secret) ise çözülmüş baytları verir. Hata varsa çağrı yerinizdedir, ki bu iyi haber: kendi diff’inizde görünür.

JWT gizli anahtarım base64 mü, UTF-8 mi?

Token’da bunu size söyleyen bir bayrak yok. Dizenin kendisi üzerinden akıl yürütmeniz gerekir:

  1. Yalnızca A–Z a–z 0–9 + / = (ya da - ve _) kullanıyor mu? Öyleyse base64 olabilir. İçinde boşluk, ! ya da # bulunan bir gizli anahtar olamaz.
  2. Uzunluğu 4’ün katı mı, ya da = dolgusuyla mı bitiyor? İkisi de girişte bir şeyin onu base64 ile kodladığına dair güçlü ipuçlarıdır.
  3. Base64 olarak çözülünce anlamlı baytlar veriyor mu? Base64 çözücü aracından geçirin. Okunabilir ASCII ya da tam olarak 32 rastgele görünümlü bayt base64’ü düşündürür. Mojibake ise dizenin hiç kodlanmadığını düşündürür.

c2VjcmV0LWtleS0xMjM0NTY3ODkwYWJjZGVm gibi bir gizli anahtar üç testi de geçer; tehlikeli olmasının nedeni tam olarak budur: belirsizdir ve iki okuma da makuldür. İçinde - ya da _ geçen gizli anahtarlar daha kötü bir biçimde belirsizdir, çünkü bunlar geçerli base64url ama geçersiz standart base64’tür.

Akıl yürüterek bir sonuca varamıyorsanız ikisini de hesaplayın. İmzalama girdisini HMAC üretici aracında iki kez HMAC-SHA256’dan geçirin: bir kez gizli anahtarı metin sayarak, bir kez çözülmüş baytlarla. Sonra iki sonucu da token’ın üçüncü parçasıyla karşılaştırın. Biri eşleşecek ve bu size sisteminizin hangi tarafının haklı olduğunu söyleyecek.

Karakterler bayt değildir

İlgili tuzak, gereksinim bayt cinsindenken karakter saymaktır. RFC 7518 §3.2 HMAC-SHA için anahtar alt sınırını karakterle değil bit’le belirtir ve kodlanmış metin genişler:

Nasıl yazdığınızEntropiBayt karşılığıHS256 için (≥256 bit ister)
32 hex karakter128 bit16 bayt❌ sınırın altında
32 base64 karakter192 bit24 bayt❌ sınırın altında
32 rastgele bayt256 bit32 bayt✅ karşılıyor (hex’te 64 karakter, dolgulu base64’te 44)

“32 karakterlik gizli anahtar” alfabeye göre 128 ile 256 bit arasında herhangi bir yerde olabilir. Bu, yukarıdaki bayt yorumu sorunundan bağımsızdır ama genellikle aynı ekipleri vurur, çünkü karakterle ölçen bir ekip baytlara hiç bakmamış bir ekiptir. Asıl seçim kuralları (uzunluk, kodlama tercihi, rotasyon) JWT gizli anahtar oluşturucu sayfasındaki notlarda duruyor.

4. Gizli anahtarın kendisi kirlenmiş

İki servisiniz bayt yorumunda anlaşıyor. İmza yine de başarısız oluyor. Şimdi her tarafın yüklediği gizli anahtarın, gerçekten yazdığınızı sandığınız gizli anahtar olup olmadığını denetleyin; çünkü ortam tesisatı fazladan bir bayt eklemekte dikkat çekici derecede başarılıdır.

.env dosyasında sondaki satır sonu. JWT_SECRET=abc satırının ardından gelen satır sonunu bazı okuyucular değere dahil eder ve gizli anahtarı abc\n olarak yükler. Tek fazladan bayt ve HMAC tümüyle alakasız bir çıktı üretir. Fark edebileceğiniz kısmi bir benzerlik olmaz.

Tırnakların veri sayılması. JWT_SECRET="abc" kimi yükleyiciler için abc, kimileri için "abc" demektir. Dosyayı bir kabuğun source etmesiyle bir kütüphanenin ayrıştırması aynı sonucu vermez. Docker Compose’un env_file özelliği ile bir .env ayrıştırıcısı aynı dosyada anlaşmazlığa düşebilir.

Kopyala-yapıştırdan gelen görünmez karakterler. Bir gizli anahtarı Slack’ten, bir wiki’den ya da bir PDF’ten kopyalamak beraberinde sıfır genişlikli boşluk (U+200B, baytları e2 80 8b) ya da bölünmez boşluk (U+00A0, baytları c2 a0) sürükleyebilir. İkisi de çoğu editörde görünmezdir ve ikisi de HMAC’i değiştirir.

CI ve konteyner bozmaları. Kabuk yorumlamasından geçen gizli anahtarlarda $ genişletilir ya da ters eğik çizgiler yenir. Bazı CI sistemleri değerleri kırpar, bazıları kırpmaz. Kubernetes secret’ları manifest’te base64, konteynerde ham hâlde durur; bu tek başına bir çift çözme tuzağıdır.

Çözüm, gizli anahtara bakmayı bırakıp onu ölçmeye başlamaktır. Her iki tarafta da değeri değil, uzunluğu ve bir parmak izini yazdırın:

printf '%s' "$JWT_SECRET" | wc -c
printf '%s' "$JWT_SECRET" | shasum -a 256 | cut -c1-16

Her iki komutu da imzalayan ve doğrulayan tarafta çalıştırıp iki çıktıyı karşılaştırın. Uzunluk ve parmak izi eşleşiyorsa sorununuz gizli anahtar değildir, Bölüm 3’e dönün. Beklediğinizden bir fazla uzunluk, sondaki satır sonudur. İki fazla uzunluk ise tırnaklardır.

Uzunluk tutmuyorsa ve içinde tam olarak ne olduğunu görmek istiyorsanız, bir geliştirme gizli anahtarına karşı yerel bir kabukta hex dökümü alın:

printf '%s' "$JWT_SECRET" | xxd

Sondaki 0a bir satır sonudur. Baştaki ve sondaki 22 bir çift tırnak karakteridir. Ortadaki c2 a0 ya da e2 80 8b görünmez karakter durumudur. Terminal çıktısını bir yere gönderen bir makinede bunu üretim gizli anahtarıyla çalıştırmayın.

Çalışan bir Node ya da Python sürecinin içindeki eşdeğer denetim:

const s = process.env.JWT_SECRET ?? '';
console.log(Buffer.byteLength(s, 'utf8'), JSON.stringify(s.slice(-3)));
import os
s = os.environ["JWT_SECRET"]
print(len(s), len(s.encode("utf-8")), repr(s[-3:]))

Python’da len(s) değerinin len(s.encode("utf-8")) değerinden küçük çıkması, ASCII olması gereken bir gizli anahtarda ASCII dışı karakterler bulunduğunu söyler.

5. Algoritma ile anahtar türü uyuşmuyor

alg başlığı ile geçtiğiniz anahtarın aynı aileye ait olması gerekir. HS256 paylaşılan bir gizli anahtar ister, yani bir bayt dizisi. RS256 ve ES256 asimetrik bir anahtar ister, yani bir PEM ya da JWK. İkisini karıştırırsanız kütüphanenin hoşgörüsüne göre ya net bir tür hatası ya da düz bir invalid signature alırsınız.

Bunun yaygın biçimleri:

  • Başlık HS256 diyor ve doğrulayıcı kütüphaneye bir PEM genel anahtarı veriyor. Bazı kütüphaneler PEM metnini HMAC’leyip imza uyuşmazlığı bildirir.
  • Başlık RS256 diyor ve doğrulayıcı ona HMAC gizli anahtar dizesini veriyor.
  • Doğrulayıcı hiç algoritma listesi geçmiyor ve kütüphanenin alg’dan çıkarım yapmasına izin veriyor; böylece imzalama tarafındaki bir yapılandırma kayması, doğrulayıcının ne yaptığını sessizce değiştiriyor.

Sonuncusu, bir yapılandırma hatasının güvenlik hatasına dönüştüğü yerdir; bu yüzden her doğrulama çağrısında algoritmayı açıkça sabitleyin:

jwt.verify(token, key, { algorithms: ['HS256'] });
jwt.decode(token, key, algorithms=["HS256"])

Sabitlemek ayrıca belirsiz imza hatalarını kesin hatalara çevirir. alg: RS256 ile bir token gelir ve izin listeniz HS256 derse, iki değeri de adıyla anan açık bir algoritma hatası alırsınız.

Bu bölümün anlattığı şey yanlış yapılandırma: kendi iki bileşeniniz anlaşamıyor, ortada saldırgan yok. Aynı biçimi taşıyan ilişkili bir durum daha var: saldırgan alg’ı RS256’dan HS256’ya çevirip genel anahtarınızı HMAC gizli anahtarı olarak kullanarak imzalıyor. Bu, algoritma karışıklığıdır; bir hata değil bir saldırıdır ve tehdit modelinin geri kalanıyla birlikte JWT güvenliği en iyi uygulamaları yazısında ele alınıyor. Savunma tesadüfen aynı: açık bir izin listesi. Yalnızca bir hatanın peşinde olsanız bile bunu uygulamak için yeterli gerekçe bu.

6. Token yolda değişti

Anahtarları suçlamadan önce, doğrulayıcının imzalayanın ürettiği dizenin aynısını aldığını doğrulayın.

Bearer öneki. Authorization: Bearer eyJhbGci... bir başlık değeridir, token değil. Yanlış karakterden bölerseniz ya da bölüp yanlış yarıyı tutarsanız, doğrulamaya Bearer eyJhbGci... ya da boş bir dize gider. Öneki açıkça soyun:

const token = req.headers.authorization?.replace(/^Bearer\s+/i, '').trim();

Boşluk ve satır sonları. Terminalden kopyalanan token’lar satır atlar. YAML içinde saklanan token’lar katlanır. Üçüncü parçanın içine gömülü tek bir \n, ayrıştırma hatası değil imza uyuşmazlığı üretir; çünkü base64url çözücüler çoğu zaman boşlukları atlar ama dize karşılaştırması atlamaz.

URL kodlaması. Sorgu parametresi olarak yolculuk etmiş bir token, . yerine %2E ile ya da fazla hevesli bir kodlayıcının çevirdiği - ve _ karakterleriyle geri dönebilir. Token’ı yalnızca bir kez çözün.

Kesilme. Cookie’ler her biri yaklaşık 4 KB’de sınırlanır ve birkaç claim taşıyan RS256 token’ları bunu rutin olarak aşar. Kesilmiş bir token genellikle base64 çözümünde patlar; ama 4 karakterlik bir sınırda kesilmişse bunun yerine elinizde geçerli görünen ama imzası yanlış bir token kalır.

İki komut bunu çözer. İyi biçimli bir JWT’de tam olarak iki nokta vardır:

printf '%s' "$TOKEN" | tr -cd '.' | wc -c

Ve her karakter base64url alfabesinde olmalıdır, yani şu hiçbir şey basmamalı:

printf '%s' "$TOKEN" | tr -d 'A-Za-z0-9._-' | xxd

İkinci komuttan gelen herhangi bir çıktı sorununuzun adını koyar: 3d orada olmaması gereken = dolgusudur, 2b ya da 2f base64url’in - ve _ beklediği yerdeki standart base64 + ve / karakterleridir, 20 ise kaçak bir boşluktur.

7. RS256 ve ES256’ya özgü hatalar

Asimetrik algoritmalar gizli anahtar sorununu anahtar yönetimi sorunuyla takas eder; hata biçimleri de buna göre değişir.

PKCS#1’e karşı PKCS#8. Bunlar aynı RSA anahtarı için iki kap biçimidir ve başlık satırındaki tek bir sözcükle gözle ayırt edilirler:

-----BEGIN RSA PRIVATE KEY-----      ← PKCS#1
-----BEGIN PRIVATE KEY-----          ← PKCS#8

Kütüphaneler hangisini kabul ettikleri konusunda birbirinden ayrılır. Biri biçimi doğrudan reddettiğinde net bir hata alırsınız; yarım ayrıştırdığında ise asla doğrulanmayan bir imza elde edebilirsiniz. Uğraşmak yerine dönüştürün:

openssl pkcs8 -topk8 -nocrypt -in pkcs1.pem -out pkcs8.pem

Anahtarlar yer değiştirmiş. Genel anahtarla imzalamak ya da özel anahtarla doğrulamak. İlkesel olarak apaçık, ama iki dosya aynı dizinde dört karakter farkla adlandırılmışken yapması kolay. Hangisinin hangisi olduğunu doğrulayın:

openssl rsa -in key.pem -noout -text | head -1

Bir özel anahtar modül boyutunu özel anahtar olarak basar; bir genel anahtar ise -pubin eklemediğiniz sürece hata verir.

JWKS ve kid kayması. Bir JWKS uç noktasıyla doğrulayıcı, token’ın kid değerini anahtar kümesiyle eşleştirerek anahtar seçer. Burada üç şey ters gider: imzalayan rotasyon yapmıştır ve doğrulayıcının önbelleğindeki JWKS bayattır; token’da kid yoktur ve doğrulayıcı kümedeki ilk anahtarı seçer; ya da iki ortam çakışan kid değerleri yayımlar. Bundan şüphelendiğinizde JWKS’i taze çekin ve token başlığındaki kid değerinin tam olarak orada bulunduğunu doğrulayın.

ES256 imza kodlaması. ECDSA imzaları r ve s olmak üzere bir tamsayı çiftidir ve bunları serileştirmenin iki yolu vardır. Genel amaçlı kripto yığınları çoğu zaman, değişken uzunluklu bir ASN.1 yapısı olan DER üretir. RFC 7518 §3.4 bunun yerine JOSE biçimini şart koşar: r ve s sabit uzunluğa dolgulanıp birleştirilir, bu da P-256 için 64 bayt eder. Bir JWT’nin içine düşmüş bir DER imzası hem yanlıştır hem de farklı uzunluktadır. Yani üçüncü parçası tam olarak 64 bayta çözülmeyen bir ES256 token’ını, dönüşümü atlayan bir şey üretmiştir.

Sorunun anahtarınızda mı yoksa hattınızda mı olduğunu ayırmak için aynı yükü JWT oluşturucu aracında bağımsız olarak imzalayın ve çıktıyı servisinizin ürettiğiyle karşılaştırın. Aynı imzalar taşımayı ya da claim işleyişini işaret eder. Farklı imzalar anahtarı işaret eder.

8. İmza hatası gibi görünen ama olmayan hatalar

Bunların bir kısmını kütüphanelerin kendisi yanlış etiketliyor; yanlış hata kaydında son bulmalarının nedeni de bu.

BelirtiAslında nedirNereye bakmalı
PyJWT ExpiredSignatureErrorexp geçmişte. Adı imza diyor; nedeni bir claim.Makineler arası saat kayması ya da fazla kısa bir TTL
PyJWT ImmatureSignatureErrornbf gelecekteİmzalayanın saati doğrulayanınkinin ilerisinde
Node TokenExpiredErrorexp geçmişteYukarıdakiyle aynı
Ayrıntısız genel 401Framework her doğrulama hatasını tek bir yanıta indirmişKütüphane düzeyinde hata günlüklemeyi açın
Birkaç dakika çalışıp sonra patlıyorToken süresi, imza değiliat ve exp değerlerini iki makinenin saatiyle karşılaştırın
Yalnızca tek bir audience için patlıyoraud ya da iss uyuşmazlığıDoğrulayıcının beklediği audience listesi

PyJWT’nin adlandırması buradaki en göze batan tuzak. ExpiredSignatureError içinde “signature” sözcüğünü taşır ama claim doğrulaması sırasında, imza çoktan başarıyla doğrulandıktan uzun süre sonra fırlatılır. Hata metnini aratmak sizi doğrudan imza sorun giderme malzemesine götürür; saatlerinizi problemin yanlış tarafında harcarsınız.

Saat kayması hepsinin en kafa karıştırıcı örüntüsünü üretir: kodunuzdaki hiçbir şeyle ilişkilenmeyen aralıklı hatalar. Bir makinenin saati ileri kayarsa yeni verilen token’lar varır varmaz nbf ya da iat doğrulamasında düşer ve kayma büyüdükçe hatalar gezinir. Önce iki makinede date -u çıktısını karşılaştırın. Çoğu kütüphane bir tolerans (leeway) parametresi kabul eder; bu, ortadan kaldıramadığınız kayma için doğru çözüm, gerçekten bozuk bir saat için yanlış çözümdür.

Genel kural: hata zamana bağlıysa, makineye bağlıysa ya da audience’a bağlıysa bu bir imza sorunu değildir. İmza hataları belirlenimcidir. Aynı token ve aynı anahtar her seferinde aynı biçimde başarısız olur.

9. Yinelenebilir bir sorun giderme akışı

Bunları sırayla çalıştırın. Her adım ya hatayı bulur ya da bir dalı eler; erken durabilmek de zaten işin amacı.

  1. Başlığı çözün. Token’ı JWT çözücü aracına yapıştırın ve alg ile kid değerlerini not edin. Bu, aşağıdaki her şeyi belirler ve hiçbir anahtar gerektirmez.
  2. Token’ın biçimini denetleyin. Tam olarak iki nokta, yalnızca base64url karakterleri, Bearer öneki yok, boşluk yok. Bölüm 6’daki iki komutu kullanın. Taşıma bozulmalarını eler.
  3. Doğrulama çağrısında algoritmayı sabitleyin. alg ile izin listeniz arasında uyuşmazlık varsa artık genel bir hata yerine ikisini de adıyla anan açık bir hata alırsınız.
  4. Anahtarın parmak izini iki tarafta da alın. Bölüm 4’teki gibi imzalayan ve doğrulayan tarafta bayt uzunluğunu ve kısaltılmış bir SHA-256 değerini yazdırın. Değerler farklıysa suç tesisatındadır ve 5. adıma hiç gelmezsiniz.
  5. İki taraf farklı dillerdeyse bayt yorumunu çözün. Bölüm 3’teki tabloya bakın, gizli anahtarın metin mi base64 mü olduğuna açıkça karar verin ve iki tarafın da bunu varsayılana bırakmak yerine kodda söylemesini sağlayın.
  6. Aynı yükü bağımsız olarak yeniden imzalayın. Doğru olduğuna inandığınız anahtarla JWT oluşturucu aracını kullanın ve üçüncü parçasını token’ınızınkiyle karşılaştırın. Eşleşme, imzalama tarafınızın sağlam olduğu ve sorunun doğrulayıcıda bulunduğu anlamına gelir.
  7. HMAC’i elle çapraz denetleyin. İmzalama girdisini HMAC üretici aracından iki bayt yorumuyla da geçirin. Hangisi token’la eşleşirse, hangi tarafı değiştireceğinizi o söyler.

Yedisini de geçip hâlâ yardıma ihtiyacınız varsa hata kaydınızı iyi yazın. Çoğu kayıt, cevabı belirleyen olguları atladığı için tıkanır. Şunları ekleyin:

  • Başlıktaki alg değeri ve bir kid bulunup bulunmadığı
  • Hem imzalayan hem doğrulayan taraftaki dil, kütüphane ve tam sürüm
  • Gizli anahtarın iki taraftaki bayt uzunluğu ve SHA-256 değerinin ilk 16 hex karakteri (asla gizli anahtarın kendisi değil)
  • Gizli anahtarın metin olarak mı base64 olarak mı saklandığı ve her tarafın onu nasıl dönüştürdüğü
  • Tam imzalama girdisi. İlk iki parça hassas değildir; token’ı elinde tutan zaten okuyabilir
  • RS256 ve ES256 için: PEM başlık satırı, birebir

Bu liste, cevaplanamaz bir “JWT imzam eşleşmiyor” sorusunu, birinin gerçekten cevaplayabileceği, üstelik çoğu zaman tek bir yanıtta cevaplayabileceği bir soruya çevirir.

SSS

Aynı gizli anahtar neden bir dilde çalışıp diğerinde patlıyor?

Çünkü kütüphaneler, bir gizli anahtar dizesinden anahtar baytlarına geçişte aynı kuralı izlemiyor. Node jsonwebtoken ve Python PyJWT UTF-8 kullanır; jjwt’nin eski String aşırı yüklemesi bir base64 codec’i kullanıyordu (jwtk/jjwt#204); Go ve .NET kararı sizin çağrı yerinize bırakır. Karakterler aynı olsa da baytlar farklı çıkar, imza da öyle.

İmza, çözülmüş yükü mü yoksa kodlanmış dizeyi mi kapsar?

Kodlanmış dizeyi. RFC 7515, imzalama girdisini birebir ASCII olarak base64url(header) + "." + base64url(payload) biçiminde tanımlar. Yükü çözüp yeniden serileştiren her katman anahtar sırasını, boşlukları ya da sayı biçimini değiştirir; bu da farklı bir dize, dolayısıyla farklı bir imza üretir.

Gizli anahtarım base64 gibi görünüyor — imzalamadan önce çözmeli miyim?

Yalnızca karşı taraf da çözüyorsa. Tek başına doğru bir cevap yok; gereksinim, iki ucun anlaşması. Dizenin yalnızca base64 karakterleri kullanıp kullanmadığına ve uzunluğunun dörde bölünüp bölünmediğine bakın, sonra tercihi varsayılanlara bırakmak yerine iki tarafta da kodda açıkça belirtin.

.env dosyasındaki sondaki satır sonu imzayı gerçekten bozabilir mi?

Evet. HMAC bayt tüketir ve abc üç baytken abc\n dört bayttır. Ortaya çıkan imzanın doğrusuyla ortak hiçbir yanı olmaz. İki makinede de printf '%s' "$JWT_SECRET" | wc -c çıktısını alın; beklenenden bir fazla uzunluk neredeyse her zaman budur.

Sorunun gizli anahtar mı yoksa algoritma mı olduğunu nasıl anlarım?

Önce başlıktan alg değerini okuyun. HS ile başlıyorsa paylaşılan bir gizli anahtara ihtiyacınız var ve bir PEM başarısız olur. RS, PS ya da ES ile başlıyorsa bir anahtar çiftine ihtiyacınız var ve bir gizli anahtar dizesi başarısız olur. alg ile anahtar türü aynı aileye girdikten sonra kalan hatalar anahtar içeriği sorunlarıdır.

jwt.io imzanın geçerli olduğunu söylerken sunucum neden reddediyor?

Çünkü çevrimiçi araç ile sunucunuz gizli anahtarı farklı yorumluyor olabilir: biri UTF-8 metni, diğeri base64 olarak. Araç, sunucunuzun türettiği baytlara karşı değil, kendi türettiği baytlara karşı doğrular. Ayrıca üretim gizli anahtarlarını asla üçüncü taraf bir siteye yapıştırmayın; bir geliştirme anahtarı kullanın.

invalid signature hatasına süresi dolmuş bir token neden olabilir mi?

Hayır. İmza doğrulaması claim doğrulamasından önce çalışır, dolayısıyla süre dolması asla neden olamaz. Süre dolması ayrıca Node’da TokenExpiredError, PyJWT’de ExpiredSignatureError olarak yüzeye çıkar — ikincisinin adı yanıltıcıdır, çünkü imza sorunsuz doğrulanmıştır ve yalnızca exp düşmüştür.

Sonuç

İmza uyuşmazlıkları neredeyse hiçbir zaman bir kriptografi sorunu değildir. HMAC-SHA256 de RSA da çalışıyor. Başarısız olan şey, bir dizenin bayta dönüştüğü sınır: bir tarafta base64 codec’i, diğer tarafta UTF-8; bir yapılandırma yükleyicisinin sakladığı bir satır sonu; bir gateway’in yardımseverce yeniden serileştirdiği bir yük. Bu rehberdeki her neden, baytlar konusunda bir anlaşmazlıktır.

O hâlde baytları açık hâle getirin ve varsayılanlara güvenmeyi bırakın. Paylaşılan gizli anahtarın ham metin olarak mı base64 olarak mı saklandığını ekibinizin belgelerine yazın ve her servisin onu, kütüphanesinin varsaydığı biçimi devralmak yerine ilan edilen yolla dönüştürmesini sağlayın. Birden çok dile yayılan sistemlerde gizli anahtarları hex ya da base64 olarak saklayıp her çağrı yerinde açıkça çözün: servis başına bir satır ve belirsizlik ortadan kalkar. Sonra Bölüm 4’teki bayt uzunluğu parmak izini sağlık denetiminize ekleyin ki bir sonraki uyuşmazlık üretimde bir 401 olarak değil, açılışta bir uyarı olarak görünsün.

Etiketler: jwt authentication debugging hmac api-security