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 verificar 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 recibir bytes o un str que solo contenga caracteres ASCII (los del inglés), esto significa que no funcionaría con caracteres como á, como en Sebastián.
Para manejar eso, primero convertimos el nombre de usuario y la contraseña 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}
🤓 Otras versiones y variantes
Consejo
Preferible usar la versión con Annotated si es posible.
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á "Incorrect username or password".
Pero luego los atacantes intentan con el nombre de usuario stanleyjobsox y la contraseña love123.
Python tendrá que comparar todo el stanleyjobso tanto en stanleyjobsox como en stanleyjobson antes de darse cuenta de que ambas strings no son iguales. Así que tomará algunos microsegundos adicionales en responder "Incorrect username or password".
En ese punto, al notar que el servidor tardó algunos microsegundos más en enviar la respuesta "Incorrect username or password", los atacantes sabrán que acertaron algo, algunas de las letras iniciales eran correctas.
Y luego pueden intentarlo de nuevo sabiendo que probablemente sea 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 contraseña correctos, con la "ayuda" de nuestra aplicación, simplemente usando el tiempo que tarda en responder.
Pero en nuestro código estamos usando secrets.compare_digest().
En resumen, tomará el mismo tiempo comparar stanleyjobsox con stanleyjobson que el que toma comparar johndoe con stanleyjobson. Y lo mismo para la contraseña.
De esa forma, usando secrets.compare_digest() en el código de tu aplicación, estará seguro contra todo este rango de ataques de seguridad.
Después de detectar que las credenciales son incorrectas, devuelve un HTTPException con un código de estado 401 (el mismo que se devuelve cuando no se proporcionan credenciales) y añade el header WWW-Authenticate para hacer que el navegador muestre el prompt de login 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}
🤓 Otras versiones y variantes
Consejo
Preferible usar la versión con Annotated si es posible.
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}