Libreria de autenticacion OIDC con Keycloak para FastAPI, Flask y Django
Project description
auth-guardian te da login OIDC, protección de endpoints y control por roles con Keycloak, listos para usar. Tú te concentras en tu app; la librería se encarga del baile de tokens, el state anti-CSRF, la introspección y el logout con revocación.
from fastapi import Depends, FastAPI
from auth_guardian import AuthGuardian, create_auth_router
app = FastAPI()
auth = AuthGuardian() # lee la config del entorno
app.include_router(create_auth_router(auth)) # /login, /oidc/callback, /logout
@app.get("/perfil")
async def perfil(user=Depends(auth.get_current_user)):
return {"hola": user["preferred_username"]}
Eso es todo lo que necesitas para tener autenticación con Keycloak. 🎉
📑 Tabla de contenidos
- ¿Por qué auth-guardian?
- Características
- Compatibilidad
- Instalación
- Configuración
- Quickstart
- Opciones del login (SSO y PKCE)
- Validación del token:
introspectionvslocal - Protección por roles
- Cómo funciona (diagramas)
- Manejo de errores
- Referencia de la API pública
- Avanzado (userinfo, multi-tenant, revocación, cookies)
- Configurar el cliente en Keycloak
- Seguridad
- Troubleshooting
- Desarrollo y contribución
- Roadmap
- Licencia
🤔 ¿Por qué auth-guardian?
Integrar OIDC con Keycloak a mano significa: construir la URL de autorización, manejar el state, intercambiar el code por tokens, validar el access_token en cada request, refrescarlo, revocarlo al salir, extraer roles… y repetirlo en cada proyecto.
auth-guardian empaqueta todo eso detrás de una API pequeña y estable, agnóstica de framework (FastAPI, Flask, Django), para que integrar Keycloak sea cuestión de un par de líneas.
✨ Características
- 🔐 Login OIDC completo — rutas
/login,/oidc/callbacky/logoutlistas para montar. - 🧩 Multi-framework — FastAPI, Flask y Django con la misma configuración.
- 🔏 PKCE (S256) por defecto — como recomiendan Keycloak 26 y OAuth 2.1, incluso para clientes confidenciales.
- 👤 Protección de endpoints —
get_current_usercomo dependencia/decorador. - 🎭 Control por roles —
require_role("admin")con roles de realm y de client. - ⚡ Dos modos de validación — introspección (revocación instantánea) o firma local (máximo rendimiento) con JWKS cacheado.
- 🚪 Logout seguro — revoca el refresh token en Keycloak antes de borrar cookies.
- 🛡️
stateanti-CSRF firmado en el flujo OIDC. - 🧾
userinfo— claims frescos del usuario directo desde Keycloak. - 🧯 Errores con contexto — los fallos traen el
error/error_descriptionOAuth exacto de Keycloak (invalid_grant, etc.) para diagnosticar en segundos. - 🚨 Fallo temprano y claro — si falta configuración, te lo dice al arrancar.
- 👥 Admin API — crear usuarios en Keycloak desde tu backend (opcional).
- 🏢 Multi-tenant — resuelve el realm por request con
tenant_resolver.
🧬 Compatibilidad
| Versiones soportadas | |
|---|---|
| Python | 3.10+ |
| FastAPI | 0.118 – 0.139+ |
| Flask | 3.x |
| Django | 4.2 – 5.x |
| Keycloak | 26 (y compatibles con OIDC estándar) |
📦 Instalación
pip install auth-guardian # FastAPI (por defecto)
pip install "auth-guardian[flask]" # Flask
pip install "auth-guardian[django]" # Django
⚙️ Configuración
auth-guardian lee estas variables de entorno. Las obligatorias hacen que la librería falle al arrancar con un mensaje claro si faltan:
| Variable | Obligatoria | Descripción |
|---|---|---|
KEYCLOAK_BASE_URL |
✅ | URL pública de Keycloak (ej. https://sso.midominio.com). |
KEYCLOAK_REALM |
✅ | Nombre del realm. |
KEYCLOAK_CLIENT_ID |
✅ | Client ID (cliente confidencial). |
KEYCLOAK_CLIENT_SECRET |
✅ | Client secret. También firma el state anti-CSRF. |
AUTH_TOKEN_VALIDATION |
— | introspection (por defecto) o local. Ver más abajo. |
IS_PROD |
— | true marca las cookies como Secure (HTTPS). Por defecto false. |
💡 También se aceptan los alias
AUTH_BASE_URL,AUTH_REALMyAUTH_CLIENT_ID.💡 Detrás de Docker/proxy puedes separar la URL pública de la interna pasando
internal_urlaAuthConfig(el navegador usa la pública; el backend, la interna).
🚀 Quickstart
FastAPI
from typing import Any
from fastapi import Depends, FastAPI
from auth_guardian import AuthGuardian, create_auth_router
app = FastAPI()
auth = AuthGuardian()
# Monta /login, /oidc/callback y /logout
app.include_router(
create_auth_router(auth, login_redirect_url="/perfil", logout_redirect_url="/login")
)
@app.get("/perfil")
async def perfil(user: dict[str, Any] = Depends(auth.get_current_user)):
return {"usuario": user["preferred_username"], "email": user.get("email")}
@app.get("/admin")
async def admin(user: dict[str, Any] = Depends(auth.require_role("admin"))):
return {"ok": True}
Flask
from flask import Flask, jsonify, g
from auth_guardian import AuthGuardian, create_flask_integration
app = Flask(__name__)
auth = AuthGuardian()
flask_auth = create_flask_integration(auth)
flask_auth.register_auth_routes(app, login_redirect_url="/perfil", logout_redirect_url="/login")
@app.get("/perfil")
@flask_auth.require_auth()
def perfil():
return jsonify(g.auth_user)
@app.get("/admin")
@flask_auth.require_role("admin")
def admin():
return jsonify({"ok": True})
Django
from django.http import JsonResponse
from django.urls import path
from auth_guardian import AuthGuardian, create_django_integration
auth = AuthGuardian()
django_auth = create_django_integration(auth)
@django_auth.require_auth()
def perfil(request):
return JsonResponse(request.auth_user)
@django_auth.require_role("admin")
def admin(request):
return JsonResponse({"ok": True})
urlpatterns = [
*django_auth.build_auth_urlpatterns(login_redirect_url="/perfil/", logout_redirect_url="/login/"),
path("perfil/", perfil),
path("admin/", admin),
]
🎛️ Opciones del login (SSO y PKCE)
Los tres frameworks aceptan las mismas opciones al registrar las rutas:
create_auth_router(
auth,
login_redirect_url="/perfil", # a dónde va el usuario tras loguearse
logout_redirect_url="/login", # a dónde va tras salir (o si el login falla)
prompt="login", # "login" fuerza credenciales SIEMPRE;
# None respeta la sesión SSO activa de Keycloak
use_pkce=True, # PKCE S256 (recomendado; actívalo salvo motivo concreto)
on_login_success=mi_hook, # callback con el payload del token tras login OK
)
💡 SSO real: con
prompt=None, un usuario con sesión activa en Keycloak entra directo sin volver a teclear credenciales. El valor por defecto"login"fuerza re-autenticación en cada login (útil para apps sensibles).
🔎 Validación del token: introspection vs local
En cada request protegido, auth-guardian valida el access_token. Elige el modo con AUTH_TOKEN_VALIDATION:
| Modo | Cómo valida | Ventaja | Coste |
|---|---|---|---|
introspection (por defecto) |
Pregunta a Keycloak (/token/introspect) en cada request |
Revocación instantánea | Una llamada de red por request |
local |
Verifica la firma del JWT contra el JWKS (cacheado, TTL 300s) | Muy rápido, sin red por request | La revocación no es instantánea (usa tokens de vida corta) |
Regla práctica:
introspectionpara máxima seguridad;localcuando el throughput importa.
🎭 Protección por roles
# Un rol
Depends(auth.require_role("admin"))
# Cualquiera de varios roles
Depends(auth.require_role("admin", "auditor"))
Considera roles de realm (realm_access.roles) y de client (resource_access). Si el usuario no tiene el rol, responde 403.
🔧 Cómo funciona
1. Login OIDC
sequenceDiagram
participant U as Usuario
participant A as Tu API
participant K as Keycloak
U->>A: GET /login
A->>K: Redirect (authorization request + state firmado)
K-->>U: Pantalla de login
U->>K: Credenciales
K-->>A: Redirect /oidc/callback?code=...
A->>K: Intercambia code por tokens
K-->>A: access_token + refresh_token
A-->>U: Set cookies + redirect a la app
2. Request protegido (introspection)
sequenceDiagram
participant C as Cliente
participant A as Tu API
participant K as Keycloak
C->>A: Request con token
A->>K: POST /token/introspect
K-->>A: active: true → 200 OK
K-->>A: active: false → 401 Unauthorized
3. Logout
sequenceDiagram
participant U as Usuario
participant A as Tu API
participant K as Keycloak
U->>A: GET /logout
A->>K: POST /revoke (refresh_token)
K-->>A: 200 / 204
A-->>U: Borra cookies + redirect
🧯 Manejo de errores
Todas las excepciones heredan de tipos claros y no filtran detalles internos al cliente:
| Excepción | Cuándo se lanza |
|---|---|
TokenValidationError |
Token inválido, expirado o emitido para otro cliente. |
KeycloakAPIError |
Fallo al hablar con Keycloak. Trae status_code, detail y el error OAuth exacto: error (p. ej. invalid_grant) y error_description. |
KeycloakAuthError |
Clase base de las anteriores. |
try:
await auth.oidc_client.refresh_access_token(refresh)
except KeycloakAPIError as exc:
# exc.error == "invalid_grant" · exc.error_description == "Token is not active"
log.warning("Refresh falló: %s (%s)", exc.error, exc.error_description)
| Situación | Respuesta al cliente |
|---|---|
active: false / token inválido |
401 Unauthorized |
| Rol insuficiente | 403 Forbidden |
| Keycloak no disponible | 503 Service Unavailable (sin exponer internos) |
from auth_guardian import KeycloakAPIError
try:
...
except KeycloakAPIError as exc:
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
📚 Referencia de la API pública
El contrato público se mantiene pequeño y estable a propósito:
| Símbolo | Qué es |
|---|---|
AuthGuardian |
Punto de entrada. Métodos: get_current_user, require_role(*roles), authenticate_token, revoke_token, startup/shutdown. |
AuthConfig |
Configuración avanzada (internal_url, issuer, token_validation, jwks_cache_ttl_seconds…). |
create_auth_router(auth, ...) |
Router de FastAPI con /login, /oidc/callback, /logout. |
create_flask_integration(auth) |
Integración Flask (register_auth_routes, require_auth, require_role). |
create_django_integration(auth) |
Integración Django (build_auth_urlpatterns, require_auth, require_role). |
AuthOIDCClient |
Cliente OIDC de bajo nivel (token, refresh, revoke, fetch_userinfo, admin API). |
extract_client_roles(payload, client_id) |
Extrae roles de client del token. |
generate_pkce_pair() |
Par PKCE (code_verifier, code_challenge) S256 para flujos propios. |
| Backends de revocación | MemoryRevocationBackend, DatabaseRevocationBackend, AutoRevocationBackend, NullRevocationBackend. |
| Excepciones | KeycloakAuthError, TokenValidationError, KeycloakAPIError. |
🧠 Avanzado
userinfo: claims frescos del usuario
info = await auth.oidc_client.fetch_userinfo(access_token)
# {"sub": "...", "email": "...", "preferred_username": "..."}
Multi-tenant (un realm por request)
Resuelve el realm dinámicamente (por subdominio, cabecera, etc.). Los clientes por realm se cachean automáticamente:
def resolver_realm(request) -> str:
return request.headers.get("X-Tenant", "realm-default")
auth = AuthGuardian(tenant_resolver=resolver_realm)
Ciclo de vida (recursos HTTP)
app = FastAPI(lifespan=auth.lifespan()) # startup/shutdown automáticos
Backends de revocación (modo local)
En validación local, los access tokens revocados con auth.revoke_token(token) se
rechazan consultando un backend por JTI:
| Backend | Uso |
|---|---|
MemoryRevocationBackend (defecto) |
Proceso único. |
DatabaseRevocationBackend |
Compartido entre workers/instancias (SQLAlchemy). |
AutoRevocationBackend |
Elige según configuración. |
NullRevocationBackend |
Desactiva el chequeo. |
Personalizar cookies
Los nombres de cookies (access_token, id_token, refresh_token) se pueden
cambiar con cookie_name, cookie_id_name y cookie_refresh_name al registrar
las rutas en cualquiera de los tres frameworks.
🔑 Configurar el cliente en Keycloak
- Crea un cliente OIDC en tu realm.
- Activa Client authentication (cliente confidencial).
- Copia el Client Secret →
KEYCLOAK_CLIENT_SECRET. - En Valid Redirect URIs, agrega la URL de tu callback (ej.
https://tuapp.com/oidc/callback). - Define los roles (de realm o de client) según tu modelo de autorización.
Detrás de un proxy inverso, asegúrate de propagar
HostyX-Forwarded-*para que las Redirect URIs coincidan.
🔒 Seguridad
- PKCE (S256) activado por defecto en el flujo de autorización, como recomiendan Keycloak 26 y OAuth 2.1 — protege el
codeincluso si es interceptado. statefirmado (HMAC-SHA256) en el flujo OIDC para prevenir CSRF.- Cookies
HttpOnly+SameSite=Lax(+SecureconIS_PROD=true); el logout revoca el refresh token en Keycloak antes de borrarlas. - Sin fuga de detalles: los errores de Keycloak se traducen a respuestas genéricas para el cliente final (el detalle queda en tus logs).
- Con
AUTH_TOKEN_VALIDATION=local, usa tokens de vida corta para acotar la ventana de revocación (la librería te avisa al arrancar si el lifespan es alto).
🩺 Troubleshooting
Missing required configuration variables
Falta una variable obligatoria. Revisa la sección Configuración y define KEYCLOAK_BASE_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID y KEYCLOAK_CLIENT_SECRET.
ModuleNotFoundError: No module named 'flask' / 'django'
Estás usando el adaptador sin instalar el extra: pip install "auth-guardian[flask]" o "auth-guardian[django]".
Introspection devuelve 401 o 403
El cliente no es confidencial o el secret es incorrecto. Verifica KEYCLOAK_CLIENT_SECRET y que Client authentication esté activo en Keycloak.
Login falla en el callback (Redirect URI mismatch)
La Redirect URI de Keycloak no coincide con la de tu app. Revisa Valid Redirect URIs y, si estás tras un proxy, las cabeceras X-Forwarded-*.
🛠️ Desarrollo y contribución
git clone <repo> && cd auth-guardian
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # tests
ruff check src # lint
mypy src # tipos
Los PRs son bienvenidos. Mantén el contrato público (__all__) pequeño y estable, y acompaña los cambios con tests.
🗺️ Roadmap
nonceOIDC en el authorization request.- Backchannel logout (propagación de logout SSO desde Keycloak).
- Device authorization flow.
- Reutilización del cliente HTTP en
AuthOIDCClient(hoy solo el validador lo reutiliza).
📄 Licencia
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file auth_guardian-0.1.35.tar.gz.
File metadata
- Download URL: auth_guardian-0.1.35.tar.gz
- Upload date:
- Size: 40.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7fb8fc352215376102abf7737692436277155740596f745621b304f8479e39f8
|
|
| MD5 |
7941c557b32165b4aa83339c490a315a
|
|
| BLAKE2b-256 |
cf73a8fac18542f662d4ec15e78adff2889f37a4a9395ad804acdbbf59c5403e
|
File details
Details for the file auth_guardian-0.1.35-py3-none-any.whl.
File metadata
- Download URL: auth_guardian-0.1.35-py3-none-any.whl
- Upload date:
- Size: 36.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33fad23ed2969d4d262d72cd6c6dbeb85225523a31472222ab950473086eaec4
|
|
| MD5 |
5495863da7fcd9f7badf017befd4bc1a
|
|
| BLAKE2b-256 |
fd6c67d7ddf162b841ba6f15f5251f260f924c26ae954ba12288f7b4c071bc3a
|