Kısa cevap: 401 Unauthorized, isteğin geçerli bir kimlik bilgisi taşımadığını söyler: Token hiç yoktur, süresi dolmuştur ya da tanınmamaktadır. Sunucu 401 ile birlikte nasıl kimlik doğrulanacağını anlatan bir WWW-Authenticate başlığı göndermek zorundadır. 403 Forbidden ise sunucunun isteği anladığını ama yerine getirmeyi reddettiğini anlatır; kimlik bilgisi varsa bile bu işlem için yetmez. Özetle 401 "kim olduğunu bilmiyorum", 403 "kim olduğunu biliyorum ama buna iznin yok" demektir. Kaynağın varlığını gizlemek isteyen sunucu 403 yerine 404 de dönebilir.

Bu yazı API tasarımı açısından iki kodu ve aralarındaki sınırı anlatıyor; bir web sitesinde tarayıcıda görülen 403 hatasının sunucu ayarlarından kaynaklanan sebepleri burada ele alınmıyor. Tanımlar RFC 9110 (HTTP Semantics) ve Bearer token kullanımını anlatan RFC 6750 metinlerine dayanıyor. Örneklerde küçük bir sipariş API'si kullanıyoruz. Serinin tamamı için REST API tasarımı öğrenme rehberi sayfasına bakın.

Kimlik doğrulama ile yetkilendirme farkı

Kimlik doğrulama ile yetkilendirme farkı

İki kavramı ayırmadan bu kodları doğru seçmek zordur. Kimlik doğrulama (authentication), isteği gönderenin kim olduğunu kanıtlama işidir: Bir API anahtarı, bir JWT ya da başka bir kimlik bilgisi bu soruyu cevaplar. Yetkilendirme (authorization) ise kimliği bilinen birinin belirli bir işi yapıp yapamayacağına karar verir: Üye rolündeki biri siparişini görebilir ama başkasının siparişini silemez.

Karışıklığın kaynağı kodun adıdır. 401'in açıklama ifadesi "Unauthorized" olsa da RFC 9110 tanımı açıkça kimlik bilgisinden söz eder: İstek, hedef kaynak için geçerli kimlik doğrulama bilgisi taşımadığı için uygulanmamıştır. Yani 401 aslında bir kimlik doğrulama hatasıdır. Yetki sorununu anlatan kod 403'tür. RFC 9110'un kimlik bilgileri bölümü bunu tek cümleyle bağlar: Geçerli ama erişim için yeterli olmayan kimlik bilgisi alan sunucu 403 ile yanıt vermelidir.

401 Unauthorized: RFC 9110 ne diyor?

RFC 9110'a göre 401 üreten sunucu, hedef kaynağa uygulanabilir en az bir "challenge" içeren WWW-Authenticate başlığını göndermek zorundadır (MUST). Challenge, bir kimlik doğrulama şeması adı ve ardından gelen parametrelerden oluşur. Şema adı büyük ve küçük harfe duyarsız bir belirteçtir; realm parametresi ise korunan alanı (protection space) tanımlamak için ayrılmıştır.

İstek zaten bir kimlik bilgisi taşıyorsa 401, bu bilginin reddedildiği anlamına gelir. İstemci yeni ya da değiştirilmiş bir Authorization başlığıyla isteği tekrarlayabilir. Standart bir ayrıntı daha ekler: İstemci en az bir kez denemiş ve yine aynı challenge ile 401 almışsa, yanıt gövdesini kullanıcıya göstermelidir, çünkü orada genellikle teşhis için faydalı bilgi vardır.

Bearer token kullanan API'ler için RFC 6750 bu çerçeveyi somutlaştırır. Kimlik bilgisi hiç yoksa sunucu yalnızca şemayı ve isteğe bağlı realm değerini gönderir; RFC 6750, istemcinin kimlik doğrulamanın gerektiğinden habersiz olduğu bu durumda hata kodu eklenmemesini önerir:

http
GET /v1/siparisler/501 HTTP/1.1
Host: api.example.com
http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="siparis-api"
Content-Type: application/json

{"error": "Kimlik bilgisi gerekli", "code": "AUTH_REQUIRED"}

Token gönderilmiş ama süresi dolmuş, iptal edilmiş ya da bozuksa RFC 6750 error="invalid_token" değerini tanımlar ve bunun için 401 önerir. İstemci yeni bir token alıp isteği tekrarlayabilir. İnsanların okuyacağı kısa bir açıklama için error_description parametresi de eklenebilir:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="siparis-api", error="invalid_token", error_description="The access token expired"
Content-Type: application/json

{"error": "Token süresi doldu", "code": "TOKEN_EXPIRED"}

403 Forbidden: Anladım ama yapmayacağım

RFC 9110'a göre 403, sunucunun isteği anladığını ama yerine getirmeyi reddettiğini belirtir. Gerekçeyi açıklamak isteyen sunucu bunu gövdede yapabilir. İstek kimlik bilgisi taşıyorsa sunucu bu bilgiyi erişim için yetersiz saymıştır. Burada istemciye yönelik açık bir öneri vardır: İstemci aynı isteği aynı kimlik bilgisiyle otomatik olarak tekrarlamamalıdır (SHOULD NOT). Farklı bir kimlikle denemesi ise serbesttir. Standart ayrıca 403'ün kimlik bilgisiyle ilgisi olmayan sebeplerle de verilebileceğini not eder.

RFC 6750 bu durum için insufficient_scope hata kodunu tanımlar: İstek, token'ın sağladığından daha yüksek bir yetki gerektirir. Sunucunun 403 dönmesi ve isteğe bağlı olarak gerekli kapsamı scope parametresinde belirtmesi önerilir. Böylece istemci, daha geniş kapsamlı bir token istemesi gerektiğini anlar:

http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="siparis-api", error="insufficient_scope", scope="siparis:sil"
Content-Type: application/json

{"error": "Bu işlem için siparis:sil izni gerekli", "code": "INSUFFICIENT_SCOPE"}

RFC 9110, WWW-Authenticate başlığının 401 dışındaki yanıtlarda da gönderilebileceğini, bunun "farklı kimlik bilgisi yanıtı değiştirebilir" anlamına geldiğini söyler. Yukarıdaki 403 örneği bu iznin kullanımıdır; 401'de ise başlık zorunludur.

404 ile kaynağı gizlemek

RFC 9110, yasaklı bir kaynağın varlığını gizlemek isteyen sunucunun 403 yerine 404 dönebileceğini açıkça yazar. 404'ün tanımı da buna uygundur: Sunucu bir temsil bulamamıştır ya da var olduğunu açıklamak istememektedir.

Bu neden önemli? Sipariş numaraları ardışık tamsayılarsa ve başkasının siparişine 403 dönüyorsanız, kötü niyetli biri numaraları tek tek deneyerek hangi siparişlerin var olduğunu öğrenebilir: 403 "var ama senin değil", 404 "yok" demektir. Kullanıcının kendisine ait olmayan kayıtları hem "yok" hem "yasak" durumunda aynı 404 ile yanıtlamak bu sızıntıyı kapatır.

Bedeli, hata ayıklamanın biraz zorlaşmasıdır. Destek ekibi "kayıt gerçekten yok mu, yoksa yetki mi eksik" sorusunu yanıt kodundan anlayamaz; bu bilgiyi sunucu günlüklerinde tutmak gerekir. Hangi kaynaklarda gizleme yapacağınızı bilinçli seçin: Kullanıcıya ait kayıtlarda 404, kullanıcının varlığını zaten bildiği yönetim işlemlerinde 403 makul bir ayrımdır.

Birden fazla şema, realm ve 407

Bir API birden fazla kimlik doğrulama yöntemini kabul edebilir; örneğin eski entegrasyonlar için Basic, yeni istemciler için Bearer. RFC 9110'a göre WWW-Authenticate başlığı birden fazla challenge içerebilir ve başlığın kendisi de birden fazla kez gönderilebilir. Standart, bazı istemcilerin tek satırda birden fazla challenge içeren değeri tanımadığını, bu yüzden bu biçimin her yerde çalışmayabileceğini not eder. Daha güvenli yol her şemayı ayrı bir satırda göndermektir:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="siparis-api"
WWW-Authenticate: Bearer realm="siparis-api"
Content-Type: application/json

{"error": "Kimlik bilgisi gerekli", "code": "AUTH_REQUIRED"}

RFC 9110 bir uyarı daha yapar: Pek çok istemci tanımadığı bir şemayı içeren challenge'ı ayrıştıramaz; geçici çözüm olarak iyi desteklenen şemaları önce listelemek önerilir. realm değeri ise sunucunun korunan kaynaklarını ayrı alanlara bölmesini sağlar. Bir istek bir realm içinde doğrulandıysa, aynı kimlik bilgisinin o realm'deki diğer isteklerde de geçerli olduğu varsayılır. Tek bir API için tek bir realm adı kullanmak çoğu durumda yeterlidir.

Son olarak 401'i 407 Proxy Authentication Required ile karıştırmayın. RFC 9110'da 407, bir proxy'nin istemciden kimlik istemesi içindir ve WWW-Authenticate yerine Proxy-Authenticate başlığıyla gelir; istemci de Authorization yerine Proxy-Authorization ile cevap verir. Kaynağın sahibi olan API sunucusu kendi kimlik kontrolü için her zaman 401 kullanır.

Karar tablosu

Karar tablosu: Authorization başlığı yok, Token süresi dolmuş, iptal edilmiş veya bozuk, Token geçerli, kapsam yetersiz, Token geçerli, rol yetersiz
DurumKodWWW-Authenticateİstemci ne yapmalı
Authorization başlığı yok401Zorunlu, hata kodu olmadanKimlik bilgisi edinip göndermeli
Token süresi dolmuş, iptal edilmiş veya bozuk401Zorunlu, error=invalid_tokenYeni token alıp bir kez tekrar denemeli
Token geçerli, kapsam yetersiz403İsteğe bağlı, error=insufficient_scopeAynı token ile tekrar denememeli
Token geçerli, rol yetersiz403GerekmezKullanıcıya yetki mesajı göstermeli
Başkasına ait kayıt, varlığı gizlenecek404GerekmezKaydın yok olduğunu varsaymalı
Authorization başlığı bozuk biçimde400RFC 6750: error=invalid_requestİsteğini düzeltmeli

Son satır RFC 6750'den gelir: Eksik ya da tekrarlanan parametre, desteklenmeyen değer veya token'ı birden fazla yoldan gönderme gibi bozuk istekler için invalid_request tanımlıdır ve 400 önerilir.

FastAPI ile 401, 403 ve 404 ayrımı

Aşağıdaki örnekte kimlik bağımlılığı (dependency) yalnızca "kim" sorusunu cevaplıyor ve hata durumunda 401 ile birlikte WWW-Authenticate başlığını gönderiyor. "Ne yapabilir" sorusu uç nokta fonksiyonlarında cevaplanıyor: Sipariş okuma başkasının kaydını 404 ile gizliyor, silme ise admin olmayan herkese 403 dönüyor. Token deposu bellekte bir sözlük; gerçek uygulamada bu bilgi paylaşılan bir veritabanında durur.

python
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.testclient import TestClient

app = FastAPI()
bearer = HTTPBearer(auto_error=False)

# Gerçek uygulamada token deposu ve sipariş tablosu veritabanında olur.
TOKENLAR = {"tok_ayse": {"kullanici": "ayse", "rol": "uye"},
            "tok_admin": {"kullanici": "zeynep", "rol": "admin"}}
IPTAL_EDILENLER = {"tok_eski"}
SIPARISLER = {501: {"id": 501, "sahip": "ayse", "tutar": 250},
              502: {"id": 502, "sahip": "mehmet", "tutar": 990}}

def kimlik(cred: HTTPAuthorizationCredentials | None = Depends(bearer)):
    if cred is None:
        raise HTTPException(401, "Kimlik bilgisi gerekli",
                            headers={"WWW-Authenticate": 'Bearer realm="siparis-api"'})
    if cred.credentials in IPTAL_EDILENLER or cred.credentials not in TOKENLAR:
        raise HTTPException(401, "Token geçersiz", headers={
            "WWW-Authenticate": 'Bearer realm="siparis-api", error="invalid_token"'})
    return TOKENLAR[cred.credentials]

@app.get("/v1/siparisler/{siparis_id}")
def siparis(siparis_id: int, k: dict = Depends(kimlik)):
    s = SIPARISLER.get(siparis_id)
    if s is None or (s["sahip"] != k["kullanici"] and k["rol"] != "admin"):
        raise HTTPException(404, "Sipariş bulunamadı")  # başkasının siparişini gizle
    return s

@app.delete("/v1/siparisler/{siparis_id}", status_code=204)
def siparis_sil(siparis_id: int, k: dict = Depends(kimlik)):
    if k["rol"] != "admin":
        raise HTTPException(403, "Bu işlem için admin rolü gerekli")
    SIPARISLER.pop(siparis_id, None)

c = TestClient(app)
r = c.get("/v1/siparisler/501")
print(r.status_code, r.headers["www-authenticate"], r.json())
# çıktı: 401 Bearer realm="siparis-api" {'detail': 'Kimlik bilgisi gerekli'}
r = c.get("/v1/siparisler/501", headers={"Authorization": "Bearer tok_eski"})
print(r.status_code, r.headers["www-authenticate"])
# çıktı: 401 Bearer realm="siparis-api", error="invalid_token"
r = c.get("/v1/siparisler/501", headers={"Authorization": "Bearer tok_ayse"})
print(r.status_code, r.json())
# çıktı: 200 {'id': 501, 'sahip': 'ayse', 'tutar': 250}
r = c.get("/v1/siparisler/502", headers={"Authorization": "Bearer tok_ayse"})
print(r.status_code, r.json())
# çıktı: 404 {'detail': 'Sipariş bulunamadı'}
r = c.delete("/v1/siparisler/501", headers={"Authorization": "Bearer tok_ayse"})
print(r.status_code, r.json())
# çıktı: 403 {'detail': 'Bu işlem için admin rolü gerekli'}
r = c.delete("/v1/siparisler/501", headers={"Authorization": "Bearer tok_admin"})
print(r.status_code)
# çıktı: 204

HTTPBearer(auto_error=False) kullanmamızın sebebi, başlık yokken FastAPI'nin kendi hatasını üretmesi yerine yanıtı bizim kurmamızdır; böylece realm ve hata kodları tamamen kontrolümüzde olur. Varsayılan davranışı merak ediyorsanız aşağıdaki deneme, denediğimiz FastAPI 0.128 sürümünde başlık yokken ve şema Bearer değilken 401 ile birlikte WWW-Authenticate: Bearer döndüğünü gösteriyor:

python
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBearer
from fastapi.testclient import TestClient

app = FastAPI()
bearer = HTTPBearer()  # auto_error=True (varsayılan)

@app.get("/v1/ben")
def ben(cred=Depends(bearer)):
    return {"token": cred.credentials}

c = TestClient(app)
r = c.get("/v1/ben")
print(r.status_code, r.headers.get("www-authenticate"), r.json())
# çıktı: 401 Bearer {'detail': 'Not authenticated'}
r = c.get("/v1/ben", headers={"Authorization": "Basic YWxpOjEyMw=="})
print(r.status_code, r.headers.get("www-authenticate"), r.json())
# çıktı: 401 Bearer {'detail': 'Not authenticated'}
r = c.get("/v1/ben", headers={"Authorization": "Bearer abc"})
print(r.status_code, r.json())
# çıktı: 200 {'token': 'abc'}

Çatıların varsayılanları sürümden sürüme değişebileceği için, istemcilerinizin bağlı olduğu kodları kendi testlerinizle sabitlemek en güvenli yoldur.

İstemci tarafında doğru tepki

Durum kodlarını doğru seçmenin asıl faydası istemcide görülür. 401 alan bir istemci, elinde yenileme imkânı varsa yeni bir token alıp isteği bir kez tekrarlayabilir; ikinci denemede de 401 alırsa döngüye girmeden kullanıcıyı yeniden oturum açmaya yönlendirmelidir. Token yenileme akışları OAuth 2.0 ve OIDC rehberinde anlatılıyor.

403 alan istemci ise aynı kimlikle tekrar denememelidir; RFC 9110'un önerisi de budur. Kullanıcıya "bu işlem için yetkiniz yok" demek ya da arayüzde ilgili düğmeyi gizlemek doğru tepkidir. 401 ile 403'ü karıştıran bir API, istemciyi ya gereksiz oturum yenilemeye ya da hiç çözülemeyecek bir hatayı tekrar tekrar denemeye iter.

Sık yapılan hatalar

Sık yapılan hatalar: Token yokken 403 dönmek., 401 ile WWW-Authenticate göndermemek., Rol yetersizliğinde 401 dönmek., Gizleme kararını tutarsız uygulamak.
  • Token yokken 403 dönmek. Kimlik bilgisi yoksa doğru kod 401'dir; istemci 403 görünce oturum açması gerektiğini anlamaz.
  • 401 ile WWW-Authenticate göndermemek. RFC 9110 bu başlığı 401 için zorunlu tutar. Başlık, istemciye hangi şemayı kullanacağını söyler.
  • Rol yetersizliğinde 401 dönmek. Kullanıcı yeniden oturum açsa da sonuç değişmez; bu durum 403'tür.
  • Gizleme kararını tutarsız uygulamak. Yok olan kayda 404, başkasının kaydına 403 dönen bir uç nokta, gizlemeye çalıştığı bilgiyi yine sızdırır.
  • Hata mesajında fazla ayrıntı vermek. "Token imzası şu anahtarla tutmadı" gibi iç ayrıntılar istemciye değil günlüklere aittir.
  • Kimlik kontrolünü bazı uç noktalarda unutmak. Kontrolü her fonksiyona tek tek yazmak yerine ortak bir bağımlılıkta toplamak bu riski azaltır. API güvenliğinin genel çerçevesi REST API güvenlik rehberinde.

Sık Sorulan Sorular

Parola yanlışsa giriş uç noktası 401 mi dönmeli?

Yanlış kimlik bilgisi RFC 9110'da 401 örnekleri arasında sayılır; giriş uç noktasında da 401 tutarlı bir seçimdir ve WWW-Authenticate başlığını unutmamak gerekir. Mesajda "e-posta veya parola hatalı" gibi genel bir ifade kullanmak, hangi e-postaların kayıtlı olduğunu açık etmez.

Hesabı askıya alınmış kullanıcıya ne dönmeliyim?

Kimlik bilgisi geçerli ama hesap işlem yapamıyorsa bu bir yetki kararıdır ve 403 uygundur. Gövdede makine okunur bir kod (örneğin ACCOUNT_SUSPENDED) vermek, istemcinin kullanıcıya doğru mesajı göstermesini sağlar.

API anahtarı ve JWT için kurallar farklı mı?

Hayır, kodların anlamı kimlik bilgisinin türünden bağımsızdır. Anahtarın nasıl üretileceği ve saklanacağı API key ile kimlik doğrulama yazısında, JWT akışı ise JWT Bearer token yazısında anlatılıyor. JWT'ye özgü saldırılar için JWT güvenlik rehberine bakın.

403 yerine 404 dönmek güvenliği tek başına sağlar mı?

Hayır. 404, yalnızca kaynağın varlığı hakkındaki bilgiyi gizler; asıl koruma, sunucunun her istekte sahiplik ve yetki kontrolünü yapmasıdır. Gizleme bu kontrolün üstüne eklenen bir katmandır.

401 ve 403 yanıtlarına gövde koymalı mıyım?

Evet. RFC 9110, 4xx yanıtlarında sunucunun hatayı açıklayan bir temsil göndermesini ve durumun geçici mi kalıcı mı olduğunu belirtmesini önerir; istemcinin de bu temsili kullanıcıya göstermesi beklenir. Gövdede makine okunur bir kod ve kısa bir açıklama yeterlidir. Bütün hatalarda tek bir biçim kullanmak istiyorsanız standart seçenek Problem Details (RFC 9457) biçimidir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: REST API tasarımı öğrenme rehberi · REST API durum kodu seçimi · API key ile kimlik doğrulama · JWT Bearer token kimlik doğrulama · JWT güvenlik rehberi · OAuth 2.0 ve OIDC · REST API güvenlik rehberi