]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
📝 Update docs for JWT to prevent timing attacks (#14908)
authorSebastián Ramírez <tiangolo@gmail.com>
Thu, 12 Feb 2026 18:10:35 +0000 (10:10 -0800)
committerGitHub <noreply@github.com>
Thu, 12 Feb 2026 18:10:35 +0000 (19:10 +0100)
docs/en/docs/tutorial/security/oauth2-jwt.md
docs_src/security/tutorial004_an_py310.py
docs_src/security/tutorial004_py310.py
docs_src/security/tutorial005_an_py310.py
docs_src/security/tutorial005_py310.py

index 95baf871c1e67e79e1d6e31136ab526b2404b1af..26894ab2873d3e66d98316aa60ee20c5404fd82c 100644 (file)
@@ -116,7 +116,11 @@ And another utility to verify if a received password matches the hash stored.
 
 And another one to authenticate and return a user.
 
-{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,56:57,60:61,70:76] *}
+{* ../../docs_src/security/tutorial004_an_py310.py hl[8,49,51,58:59,62:63,72:79] *}
+
+When `authenticate_user` is called with a username that doesn't exist in the database, we still run `verify_password` against a dummy hash.
+
+This ensures the endpoint takes roughly the same amount of time to respond whether the username is valid or not, preventing **timing attacks** that could be used to enumerate existing usernames.
 
 /// note
 
@@ -152,7 +156,7 @@ Define a Pydantic Model that will be used in the token endpoint for the response
 
 Create a utility function to generate a new access token.
 
-{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,79:87] *}
+{* ../../docs_src/security/tutorial004_an_py310.py hl[4,7,13:15,29:31,82:90] *}
 
 ## Update the dependencies { #update-the-dependencies }
 
@@ -162,7 +166,7 @@ Decode the received token, verify it, and return the current user.
 
 If the token is invalid, return an HTTP error right away.
 
-{* ../../docs_src/security/tutorial004_an_py310.py hl[90:107] *}
+{* ../../docs_src/security/tutorial004_an_py310.py hl[93:110] *}
 
 ## Update the `/token` *path operation* { #update-the-token-path-operation }
 
@@ -170,7 +174,7 @@ Create a `timedelta` with the expiration time of the token.
 
 Create a real JWT access token and return it.
 
-{* ../../docs_src/security/tutorial004_an_py310.py hl[118:133] *}
+{* ../../docs_src/security/tutorial004_an_py310.py hl[121:136] *}
 
 ### Technical details about the JWT "subject" `sub` { #technical-details-about-the-jwt-subject-sub }
 
index 368c743bf921a265aed385aeb7d1936cb152793a..685cb034eeeb03432c0fef40b29237260dc0ef1a 100644 (file)
@@ -48,6 +48,8 @@ class UserInDB(User):
 
 password_hash = PasswordHash.recommended()
 
+DUMMY_HASH = password_hash.hash("dummypassword")
+
 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 app = FastAPI()
@@ -70,6 +72,7 @@ def get_user(db, username: str):
 def authenticate_user(fake_db, username: str, password: str):
     user = get_user(fake_db, username)
     if not user:
+        verify_password(password, DUMMY_HASH)
         return False
     if not verify_password(password, user.hashed_password):
         return False
index 8d0785b404cd13a100e90beaa45f68848f1e7eb4..dc7f1c9e296dd8773b6d2b2eeb40352bab4410ff 100644 (file)
@@ -47,6 +47,8 @@ class UserInDB(User):
 
 password_hash = PasswordHash.recommended()
 
+DUMMY_HASH = password_hash.hash("dummypassword")
+
 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 app = FastAPI()
@@ -69,6 +71,7 @@ def get_user(db, username: str):
 def authenticate_user(fake_db, username: str, password: str):
     user = get_user(fake_db, username)
     if not user:
+        verify_password(password, DUMMY_HASH)
         return False
     if not verify_password(password, user.hashed_password):
         return False
index fef0ab71ca2fee0f5ef2b18600c9a7506a25011b..9911723db7dbc16e0c4633e33c18e9029fe1089f 100644 (file)
@@ -60,6 +60,8 @@ class UserInDB(User):
 
 password_hash = PasswordHash.recommended()
 
+DUMMY_HASH = password_hash.hash("dummypassword")
+
 oauth2_scheme = OAuth2PasswordBearer(
     tokenUrl="token",
     scopes={"me": "Read information about the current user.", "items": "Read items."},
@@ -85,6 +87,7 @@ def get_user(db, username: str):
 def authenticate_user(fake_db, username: str, password: str):
     user = get_user(fake_db, username)
     if not user:
+        verify_password(password, DUMMY_HASH)
         return False
     if not verify_password(password, user.hashed_password):
         return False
index 412fbf798484db67a1acacc6e92c6559b4f62f76..710cdac329ca340c05d9d180bff31fb4614a59ca 100644 (file)
@@ -59,6 +59,8 @@ class UserInDB(User):
 
 password_hash = PasswordHash.recommended()
 
+DUMMY_HASH = password_hash.hash("dummypassword")
+
 oauth2_scheme = OAuth2PasswordBearer(
     tokenUrl="token",
     scopes={"me": "Read information about the current user.", "items": "Read items."},
@@ -84,6 +86,7 @@ def get_user(db, username: str):
 def authenticate_user(fake_db, username: str, password: str):
     user = get_user(fake_db, username)
     if not user:
+        verify_password(password, DUMMY_HASH)
         return False
     if not verify_password(password, user.hashed_password):
         return False