Skip to main content

Libreria de autenticacion OIDC con Keycloak para FastAPI, Flask y Django

Project description

🛡️ Auth Guardian

Integra FastAPI, Flask o Django con Keycloak en minutos — sin pelearte con OIDC.

PyPI version Python versions License Keycloak 26


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?

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/callback y /logout listas 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 endpointsget_current_user como dependencia/decorador.
  • 🎭 Control por rolesrequire_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.
  • 🛡️ state anti-CSRF firmado en el flujo OIDC.
  • 🧾 userinfo — claims frescos del usuario directo desde Keycloak.
  • 🧯 Errores con contexto — los fallos traen el error/error_description OAuth 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_REALM y AUTH_CLIENT_ID.

💡 Detrás de Docker/proxy puedes separar la URL pública de la interna pasando internal_url a AuthConfig (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: introspection para máxima seguridad; local cuando 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

  1. Crea un cliente OIDC en tu realm.
  2. Activa Client authentication (cliente confidencial).
  3. Copia el Client SecretKEYCLOAK_CLIENT_SECRET.
  4. En Valid Redirect URIs, agrega la URL de tu callback (ej. https://tuapp.com/oidc/callback).
  5. 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 Host y X-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 code incluso si es interceptado.
  • state firmado (HMAC-SHA256) en el flujo OIDC para prevenir CSRF.
  • Cookies HttpOnly + SameSite=Lax (+ Secure con IS_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

  • nonce OIDC 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

auth_guardian-0.1.35.tar.gz (40.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

auth_guardian-0.1.35-py3-none-any.whl (36.1 kB view details)

Uploaded Python 3

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

Hashes for auth_guardian-0.1.35.tar.gz
Algorithm Hash digest
SHA256 7fb8fc352215376102abf7737692436277155740596f745621b304f8479e39f8
MD5 7941c557b32165b4aa83339c490a315a
BLAKE2b-256 cf73a8fac18542f662d4ec15e78adff2889f37a4a9395ad804acdbbf59c5403e

See more details on using hashes here.

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

Hashes for auth_guardian-0.1.35-py3-none-any.whl
Algorithm Hash digest
SHA256 33fad23ed2969d4d262d72cd6c6dbeb85225523a31472222ab950473086eaec4
MD5 5495863da7fcd9f7badf017befd4bc1a
BLAKE2b-256 fd6c67d7ddf162b841ba6f15f5251f260f924c26ae954ba12288f7b4c071bc3a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page