Saltar al contenido

Seguridad - Primeros Pasos

Imaginemos que tienes tu API de backend en algún dominio.

Y tienes un frontend en otro dominio o en una ruta diferente del mismo dominio (o en una aplicación móvil).

Y quieres tener una forma de que el frontend se autentique con el backend, usando un usuario y contraseña.

Podemos usar OAuth2 para construir eso con FastAPI.

Pero ahorrémonos el tiempo de leer la especificación completa solo para encontrar esos pequeños fragmentos de información que necesitas.

Usemos las herramientas proporcionadas por FastAPI para manejar la seguridad.

Cómo se ve

Primero usemos el código y veamos cómo funciona, y luego volveremos para entender qué está pasando.

Crear main.py

Copia el ejemplo en un archivo main.py:

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
🤓 Otras versiones y variantes

Consejo

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

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Ejecútalo

Nota

El paquete python-multipart se instala automáticamente con FastAPI cuando ejecutas el comando pip install "fastapi[standard]".

Sin embargo, si usas el comando pip install fastapi, el paquete python-multipart no está incluido por defecto.

Para instalarlo manualmente, asegúrate de crear un entorno virtual, activarlo, y luego instalarlo con:

$ pip install python-multipart

Esto es porque OAuth2 usa "form data" para enviar el usuario y la contraseña.

Ejecuta el ejemplo con:

fast →fastapi dev
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

restart ↻

Revísalo

Ve a la documentación interactiva en: http://127.0.0.1:8000/docs.

Verás algo como esto:

¡Botón Authorize!

Ya tienes un flamante botón "Authorize".

Y tu path operation tiene un pequeño candado en la esquina superior derecha en el que puedes hacer clic.

Y si haces clic, tienes un pequeño formulario de autorización para escribir un usuario y contraseña (y otros campos opcionales):

Nota

No importa lo que escribas en el formulario, no funcionará todavía. Pero llegaremos ahí.

Esto por supuesto no es el frontend para los usuarios finales, pero es una gran herramienta automática para documentar interactivamente toda tu API.

Puede ser usado por el equipo frontend (que también puedes ser tú).

Puede ser usado por aplicaciones y sistemas de terceros.

Y también puedes usarlo tú mismo, para depurar, verificar y probar la misma aplicación.

El flujo password

Ahora volvamos un poco atrás y entendamos qué es todo eso.

El "flujo" password es una de las formas ("flujos") definidas en OAuth2, para manejar la seguridad y la autenticación.

OAuth2 fue diseñado para que el backend o la API pudieran ser independientes del servidor que autentica al usuario.

Pero en este caso, la misma aplicación FastAPI manejará la API y la autenticación.

Así que, revisémoslo desde ese punto de vista simplificado:

  • El usuario escribe el usuario y la contraseña en el frontend, y presiona Enter.
  • El frontend (ejecutándose en el navegador del usuario) envía ese usuario y contraseña a una URL específica en nuestra API (declarada con tokenUrl="token").
  • The API checks that username and password, and responds with a "token" (we haven't implemented any of this yet).
    • Un "token" es simplemente un string con algún contenido que podemos usar después para verificar a este usuario.
    • Normally, a token is set to expire after some time.
      • Así que, el usuario tendrá que iniciar sesión de nuevo en algún momento después.
      • Y si el token es robado, el riesgo es menor. No es como una llave permanente que funcionará para siempre (en la mayoría de los casos).
  • El frontend almacena ese token temporalmente en algún lugar.
  • El usuario hace clic en el frontend para ir a otra sección de la aplicación web frontend.
  • The frontend needs to fetch some more data from the API.
    • Pero necesita autenticación para ese endpoint específico.
    • Así que, para autenticarse con nuestra API, envía un header Authorization con un valor de Bearer más el token.
    • Si el token contiene foobar, el contenido del header Authorization sería: Bearer foobar.

OAuth2PasswordBearer de FastAPI

FastAPI proporciona varias herramientas, a diferentes niveles de abstracción, para implementar estas características de seguridad.

En este ejemplo vamos a usar OAuth2, con el flujo Password, usando un token Bearer. Hacemos eso usando la clase OAuth2PasswordBearer.

Nota

Un token "bearer" no es la única opción.

Pero es la mejor para nuestro caso de uso.

Y podría ser la mejor para la mayoría de los casos de uso, a menos que seas un experto en OAuth2 y sepas exactamente por qué hay otra opción que se adapta mejor a tus necesidades.

En ese caso, FastAPI también te proporciona las herramientas para construirlo.

Cuando creamos una instancia de la clase OAuth2PasswordBearer pasamos el parámetro tokenUrl. Este parámetro contiene la URL que el cliente (el frontend ejecutándose en el navegador del usuario) usará para enviar el usuario y la contraseña para obtener un token.

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
🤓 Otras versiones y variantes

Consejo

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

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Consejo

Aquí tokenUrl="token" se refiere a una URL relativa token que no hemos creado todavía. Como es una URL relativa, es equivalente a ./token.

Como estamos usando una URL relativa, si tu API estuviera ubicada en https://example.com/, entonces se referiría a https://example.com/token. Pero si tu API estuviera ubicada en https://example.com/api/v1/, entonces se referiría a https://example.com/api/v1/token.

Usar una URL relativa es importante para asegurar que tu aplicación siga funcionando incluso en un caso de uso avanzado como Behind a Proxy (Detrás de un Proxy).

Este parámetro no crea ese endpoint / path operation, sino que declara que la URL /token será la que el cliente debería usar para obtener el token. Esa información se usa en OpenAPI, y luego en los sistemas de documentación interactiva de la API.

Pronto también crearemos la path operation real.

Nota

Si eres un "Pythonista" muy estricto, podría no gustarte el estilo del nombre del parámetro tokenUrl en lugar de token_url.

Eso es porque usa el mismo nombre que en la especificación OpenAPI. De manera que si necesitas investigar más sobre cualquiera de estos esquemas de seguridad puedes simplemente copiarlo y pegarlo para encontrar más información sobre él.

La variable oauth2_scheme es una instancia de OAuth2PasswordBearer, pero también es un "callable".

Se podría llamar como:

oauth2_scheme(some, parameters)

Así que, puede ser usada con Depends.

Usarlo

Ahora puedes pasar ese oauth2_scheme en una dependencia con Depends.

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
🤓 Otras versiones y variantes

Consejo

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

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Esta dependencia proporcionará un str que se asigna al parámetro token de la función de path operation.

FastAPI sabrá que puede usar esta dependencia para definir un "esquema de seguridad" en el esquema OpenAPI (y la documentación automática de la API).

Detalles Técnicos

FastAPI sabrá que puede usar la clase OAuth2PasswordBearer (declarada en una dependencia) para definir el esquema de seguridad en OpenAPI porque hereda de fastapi.security.oauth2.OAuth2, que a su vez hereda de fastapi.security.base.SecurityBase.

Todas las utilidades de seguridad que se integran con OpenAPI (y la documentación automática de la API) heredan de SecurityBase, así es como FastAPI puede saber cómo integrarlas en OpenAPI.

Qué hace

Irá y buscará en la petición ese header Authorization, verificará si el valor es Bearer más algún token, y devolverá el token como un str.

Si no ve un header Authorization, o el valor no tiene un token Bearer, responderá con un error de código de estado 401 (UNAUTHORIZED) directamente.

Ni siquiera tienes que verificar si el token existe para devolver un error. Puedes estar seguro de que si tu función se ejecuta, tendrá un str en ese token.

Ya puedes probarlo en la documentación interactiva:

No estamos verificando la validez del token todavía, pero eso ya es un comienzo.

Resumen

Así, con solo 3 o 4 líneas extra, ya tienes alguna forma primitiva de seguridad.