Cuando intentas abrir la URL por primera vez (o haces clic en el botón "Execute" en la documentación) el navegador te pedirá tu nombre de usuario y contraseña:
Usa una dependencia para comprobar si el nombre de usuario y la contraseña son correctos.
Para esto, usa el módulo estándar de Python secrets para verificar el nombre de usuario y la contraseña.
secrets.compare_digest() necesita tomar bytes o un str que solo contenga caracteres ASCII (los carácteres en inglés), esto significa que no funcionaría con caracteres como á, como en Sebastián.
Para manejar eso, primero convertimos el username y password a bytes codificándolos con UTF-8.
Luego podemos usar secrets.compare_digest() para asegurar que credentials.username es "stanleyjobson", y que credentials.password es "swordfish".
importsecretsfromtypingimportAnnotatedfromfastapiimportDepends,FastAPI,HTTPException,statusfromfastapi.securityimportHTTPBasic,HTTPBasicCredentialsapp=FastAPI()security=HTTPBasic()defget_current_username(credentials:Annotated[HTTPBasicCredentials,Depends(security)],):current_username_bytes=credentials.username.encode("utf8")correct_username_bytes=b"stanleyjobson"is_correct_username=secrets.compare_digest(current_username_bytes,correct_username_bytes)current_password_bytes=credentials.password.encode("utf8")correct_password_bytes=b"swordfish"is_correct_password=secrets.compare_digest(current_password_bytes,correct_password_bytes)ifnot(is_correct_usernameandis_correct_password):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Incorrect username or password",headers={"WWW-Authenticate":"Basic"},)returncredentials.username@app.get("/users/me")defread_current_user(username:Annotated[str,Depends(get_current_username)]):return{"username":username}
🤓 Other versions and variants
importsecretsfromfastapiimportDepends,FastAPI,HTTPException,statusfromfastapi.securityimportHTTPBasic,HTTPBasicCredentialsfromtyping_extensionsimportAnnotatedapp=FastAPI()security=HTTPBasic()defget_current_username(credentials:Annotated[HTTPBasicCredentials,Depends(security)],):current_username_bytes=credentials.username.encode("utf8")correct_username_bytes=b"stanleyjobson"is_correct_username=secrets.compare_digest(current_username_bytes,correct_username_bytes)current_password_bytes=credentials.password.encode("utf8")correct_password_bytes=b"swordfish"is_correct_password=secrets.compare_digest(current_password_bytes,correct_password_bytes)ifnot(is_correct_usernameandis_correct_password):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Incorrect username or password",headers={"WWW-Authenticate":"Basic"},)returncredentials.username@app.get("/users/me")defread_current_user(username:Annotated[str,Depends(get_current_username)]):return{"username":username}
Tip
Prefer to use the Annotated version if possible.
importsecretsfromfastapiimportDepends,FastAPI,HTTPException,statusfromfastapi.securityimportHTTPBasic,HTTPBasicCredentialsapp=FastAPI()security=HTTPBasic()defget_current_username(credentials:HTTPBasicCredentials=Depends(security)):current_username_bytes=credentials.username.encode("utf8")correct_username_bytes=b"stanleyjobson"is_correct_username=secrets.compare_digest(current_username_bytes,correct_username_bytes)current_password_bytes=credentials.password.encode("utf8")correct_password_bytes=b"swordfish"is_correct_password=secrets.compare_digest(current_password_bytes,correct_password_bytes)ifnot(is_correct_usernameandis_correct_password):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Incorrect username or password",headers={"WWW-Authenticate":"Basic"},)returncredentials.username@app.get("/users/me")defread_current_user(username:str=Depends(get_current_username)):return{"username":username}
Esto sería similar a:
ifnot(credentials.username=="stanleyjobson")ornot(credentials.password=="swordfish"):# Return some error...
Pero al usar secrets.compare_digest() será seguro contra un tipo de ataques llamados "timing attacks".
Pero justo en el momento en que Python compara la primera j en johndoe con la primera s en stanleyjobson, devolverá False, porque ya sabe que esas dos strings no son iguales, pensando que "no hay necesidad de gastar más computación comparando el resto de las letras". Y tu aplicación dirá "Nombre de usuario o contraseña incorrectos".
Pero luego los atacantes prueban con el nombre de usuario stanleyjobsox y contraseña love123.
Python tendrá que comparar todo stanleyjobso en ambos stanleyjobsox y stanleyjobson antes de darse cuenta de que ambas strings no son las mismas. Así que tomará algunos microsegundos extra para responder "Nombre de usuario o contraseña incorrectos".
En ese punto, al notar que el servidor tardó algunos microsegundos más en enviar el response "Nombre de usuario o contraseña incorrectos", los atacantes sabrán que acertaron en algo, algunas de las letras iniciales eran correctas.
Y luego pueden intentar de nuevo sabiendo que probablemente es algo más similar a stanleyjobsox que a johndoe.
Por supuesto, los atacantes no intentarían todo esto a mano, escribirían un programa para hacerlo, posiblemente con miles o millones de pruebas por segundo. Y obtendrían solo una letra correcta adicional a la vez.
Pero haciendo eso, en algunos minutos u horas, los atacantes habrían adivinado el nombre de usuario y la contraseña correctos, con la "ayuda" de nuestra aplicación, solo usando el tiempo tomado para responder.
Después de detectar que las credenciales son incorrectas, regresa un HTTPException con un código de estado 401 (el mismo que se devuelve cuando no se proporcionan credenciales) y agrega el header WWW-Authenticate para que el navegador muestre el prompt de inicio de sesión nuevamente:
importsecretsfromtypingimportAnnotatedfromfastapiimportDepends,FastAPI,HTTPException,statusfromfastapi.securityimportHTTPBasic,HTTPBasicCredentialsapp=FastAPI()security=HTTPBasic()defget_current_username(credentials:Annotated[HTTPBasicCredentials,Depends(security)],):current_username_bytes=credentials.username.encode("utf8")correct_username_bytes=b"stanleyjobson"is_correct_username=secrets.compare_digest(current_username_bytes,correct_username_bytes)current_password_bytes=credentials.password.encode("utf8")correct_password_bytes=b"swordfish"is_correct_password=secrets.compare_digest(current_password_bytes,correct_password_bytes)ifnot(is_correct_usernameandis_correct_password):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Incorrect username or password",headers={"WWW-Authenticate":"Basic"},)returncredentials.username@app.get("/users/me")defread_current_user(username:Annotated[str,Depends(get_current_username)]):return{"username":username}
🤓 Other versions and variants
importsecretsfromfastapiimportDepends,FastAPI,HTTPException,statusfromfastapi.securityimportHTTPBasic,HTTPBasicCredentialsfromtyping_extensionsimportAnnotatedapp=FastAPI()security=HTTPBasic()defget_current_username(credentials:Annotated[HTTPBasicCredentials,Depends(security)],):current_username_bytes=credentials.username.encode("utf8")correct_username_bytes=b"stanleyjobson"is_correct_username=secrets.compare_digest(current_username_bytes,correct_username_bytes)current_password_bytes=credentials.password.encode("utf8")correct_password_bytes=b"swordfish"is_correct_password=secrets.compare_digest(current_password_bytes,correct_password_bytes)ifnot(is_correct_usernameandis_correct_password):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Incorrect username or password",headers={"WWW-Authenticate":"Basic"},)returncredentials.username@app.get("/users/me")defread_current_user(username:Annotated[str,Depends(get_current_username)]):return{"username":username}
Tip
Prefer to use the Annotated version if possible.
importsecretsfromfastapiimportDepends,FastAPI,HTTPException,statusfromfastapi.securityimportHTTPBasic,HTTPBasicCredentialsapp=FastAPI()security=HTTPBasic()defget_current_username(credentials:HTTPBasicCredentials=Depends(security)):current_username_bytes=credentials.username.encode("utf8")correct_username_bytes=b"stanleyjobson"is_correct_username=secrets.compare_digest(current_username_bytes,correct_username_bytes)current_password_bytes=credentials.password.encode("utf8")correct_password_bytes=b"swordfish"is_correct_password=secrets.compare_digest(current_password_bytes,correct_password_bytes)ifnot(is_correct_usernameandis_correct_password):raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Incorrect username or password",headers={"WWW-Authenticate":"Basic"},)returncredentials.username@app.get("/users/me")defread_current_user(username:str=Depends(get_current_username)):return{"username":username}