Saltar al contenido

Autenticación Básica HTTP

Para los casos más simples, puedes usar Autenticación Básica HTTP.

En la Autenticación Básica HTTP, la aplicación espera un header que contiene un nombre de usuario y una contraseña.

Si no lo recibe, devuelve un error HTTP 401 "Unauthorized".

Y devuelve un header WWW-Authenticate con un valor de Basic, y un parámetro opcional realm.

Eso le dice al navegador que muestre el prompt integrado para un nombre de usuario y una contraseña.

Luego, cuando escribes ese nombre de usuario y contraseña, el navegador los envía en el header automáticamente.

Autenticación Básica HTTP Simple

  • Importa HTTPBasic y HTTPBasicCredentials.
  • Crea un esquema de "security" usando HTTPBasic.
  • Usa ese security con una dependencia en tu operación de ruta.
  • It returns an object of type HTTPBasicCredentials:
    • Contiene el nombre de usuario y la contraseña enviados.
from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
    return {"username": credentials.username, "password": credentials.password}
🤓 Otras versiones y variantes

Consejo

Preferible usar la versión con Annotated si es posible.

from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

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:

Verificar el nombre de usuario

Aquí hay un ejemplo más completo.

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".

import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_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
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_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.

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_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
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}

Esto sería similar a:

if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"):
    # Return some error
    ...

Pero al usar secrets.compare_digest() será seguro contra un tipo de ataques llamados "timing attacks".

Ataques de Timing

¿Pero qué es un "timing attack"?

Imaginemos que algunos atacantes están intentando adivinar el nombre de usuario y la contraseña.

Y envían una petición con un nombre de usuario johndoe y una contraseña love123.

Entonces el código de Python en tu aplicación sería equivalente a algo como:

if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
    ...

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.

Y el código de tu aplicación hace algo como:

if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
    ...

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".

El tiempo de respuesta ayuda a los atacantes

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.

Un ataque "profesional"

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.

Arreglarlo con secrets.compare_digest()

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.

Devolver el error

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:

import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_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
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_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.

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_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
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}