Metadata-Version: 2.4
Name: auth-guardian
Version: 0.1.26
Summary: Libreria de autenticacion OIDC con Keycloak para FastAPI
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx<0.29,>=0.28
Requires-Dist: python-jose[cryptography]<4,>=3.5
Provides-Extra: database
Requires-Dist: sqlalchemy<3,>=2.0; extra == "database"
Requires-Dist: aiosqlite<1,>=0.20; extra == "database"
Provides-Extra: fastapi
Requires-Dist: fastapi<0.119,>=0.118; extra == "fastapi"
Provides-Extra: dev
Requires-Dist: build<2,>=1.3; extra == "dev"
Requires-Dist: fastapi<0.119,>=0.118; extra == "dev"
Requires-Dist: mypy<2,>=1.10; extra == "dev"
Requires-Dist: pytest<9,>=8.3; extra == "dev"
Requires-Dist: pytest-asyncio<2,>=1.2; extra == "dev"
Requires-Dist: respx<0.23,>=0.22; extra == "dev"
Requires-Dist: ruff<0.15,>=0.14; extra == "dev"
Requires-Dist: twine<7,>=6.2; extra == "dev"

# Auth Guardian

<p align="center">
  <img src="https://img.shields.io/pypi/v/auth-guardian?color=blue&label=PyPI" alt="PyPI version"/>
  <img src="https://img.shields.io/pypi/pyversions/auth-guardian" alt="Python versions"/>
  <img src="https://img.shields.io/pypi/l/auth-guardian" alt="License"/>
</p>

> Librería para integrar FastAPI con Keycloak sin complicarte con OIDC desde cero.

---

## ¿Para qué sirve?

Con `auth-guardian` tienes listo:

- Login OIDC con Keycloak.
- Rutas `/login`, `/oidc/callback` y `/logout`.
- Protección de endpoints autenticados.
- Protección por rol (`admin`, etc.).
- Validación en tiempo real por introspection.
- Logout con revocación inmediata del refresh token.

---

## Instalación

```bash
pip install auth-guardian
```

**Requisitos:** Python >= 3.10

---

## Configuración rápida

### 1. Variables de entorno obligatorias

La librería **falla al iniciar con un mensaje claro** si falta alguna de estas:

```env
KEYCLOAK_BASE_URL=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=
```

> `KEYCLOAK_CLIENT_SECRET` se usa tanto para introspection como para firmar/verificar el `state` anti-CSRF.

### 2. Configuración en Keycloak

Antes de arrancar, verifica que tu cliente en Keycloak esté configurado correctamente:

1. Crear cliente de tipo **OIDC**.
2. Activar **Client authentication** (cliente confidencial).
3. Configurar el **Client Secret**.
4. Agregar la **Redirect URI** de tu app para el callback OIDC.
5. Asignar **roles** en realm o client según tu modelo de autorización.

---

## Integración con FastAPI

Crea un archivo `auth.py` y pega esto:

```python
from __future__ import annotations

import os
from typing import Any

from fastapi import Depends, FastAPI, HTTPException, status
from pydantic import BaseModel, Field

from auth_guardian import AuthGuardian, KeycloakAPIError, create_auth_router

KEYCLOAK_ADMIN_CLIENT_ID = os.getenv("KEYCLOAK_ADMIN_CLIENT_ID", "")
KEYCLOAK_ADMIN_CLIENT_SECRET = os.getenv("KEYCLOAK_ADMIN_CLIENT_SECRET", "")

app = FastAPI(title="AuthGuardian Demo")
auth = AuthGuardian()

app.include_router(
    create_auth_router(
        auth=auth,
        login_redirect_url="/profile",
        logout_redirect_url="/login",
    )
)


class RegisterRequest(BaseModel):
    username: str = Field(min_length=3, max_length=64)
    email: str = Field(min_length=5, max_length=128)
    password: str = Field(min_length=8, max_length=128)
    first_name: str = ""
    last_name: str = ""


@app.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/profile")
async def profile(user: dict[str, Any] = Depends(auth.get_current_user)) -> dict[str, Any]:
    return {
        "mensaje": f"Hola {user.get('preferred_username')}",
        "email": user.get("email"),
        "sub": user.get("sub"),
    }


@app.get("/admin")
async def admin(user: dict[str, Any] = Depends(auth.require_role("admin"))) -> dict[str, str]:
    return {"mensaje": "Acceso permitido para rol admin"}


@app.post("/register", status_code=status.HTTP_201_CREATED)
async def register(payload: RegisterRequest) -> dict[str, Any]:
    try:
        user = await auth.oidc_client.create_user_via_admin_api(
            admin_client_id=KEYCLOAK_ADMIN_CLIENT_ID,
            admin_client_secret=KEYCLOAK_ADMIN_CLIENT_SECRET,
            username=payload.username,
            email=payload.email,
            first_name=payload.first_name,
            last_name=payload.last_name,
            password=payload.password,
            enabled=True,
            email_verified=False,
        )
    except KeycloakAPIError as exc:
        raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc

    return {"mensaje": "Usuario creado correctamente", "usuario": user}
```

Levanta la app:

```bash
uvicorn auth:app --reload
```

---

## Flujos de autenticación

### 1. Login OIDC

El usuario accede a `/login` y es redirigido al servidor Keycloak. Una vez que ingresa sus credenciales, Keycloak devuelve un `code` al callback, que la librería intercambia por los tokens.

![Flujo de Login OIDC](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBVIGFzIFVzdWFyaW8KICAgIHBhcnRpY2lwYW50IEEgYXMgQVBJIEZhc3RBUEkKICAgIHBhcnRpY2lwYW50IEsgYXMgS2V5Y2xvYWsKCiAgICBVLT4-QTogR0VUIC9sb2dpbgogICAgQS0-Pks6IFJlZGlyZWN0IEF1dGhvcml6YXRpb24gUmVxdWVzdAogICAgSy0tPj5VOiBMb2dpbiBVSQogICAgVS0-Pks6IENyZWRlbmNpYWxlcwogICAgSy0tPj5BOiBSZWRpcmVjdCAvb2lkYy9jYWxsYmFjaz9jb2RlPS4uLgogICAgQS0-Pks6IEV4Y2hhbmdlIGNvZGUgcG9yIHRva2VucwogICAgSy0tPj5BOiBhY2Nlc3NfdG9rZW4gKyByZWZyZXNoX3Rva2VuCiAgICBBLS0-PlU6IFNldCBjb29raWVzICsgcmVkaXJlY3QgYXBw)

---

### 2. Request protegido (introspection)

En cada request a un endpoint protegido, el token del cliente es validado **en tiempo real** contra Keycloak. No hay caché ni validación local por defecto.

![Flujo de Introspection](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENsaWVudGUKICAgIHBhcnRpY2lwYW50IEEgYXMgQVBJCiAgICBwYXJ0aWNpcGFudCBLIGFzIEtleWNsb2FrCgogICAgQy0-PkE6IFJlcXVlc3QgY29uIHRva2VuCiAgICBBLT4-SzogUE9TVCAvdG9rZW4vaW50cm9zcGVjdAogICAgSy0tPj5BOiBhY3RpdmU6IHRydWUgIOKGkiAyMDAgT0sKICAgIEstLT4-QTogYWN0aXZlOiBmYWxzZSDihpIgNDAxIFVuYXV0aG9yaXplZA==)

---

### 3. Logout

El logout revoca el refresh token en Keycloak **antes** de borrar las cookies, garantizando que la sesión quede inválida de inmediato en el servidor.

![Flujo de Logout](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBVIGFzIFVzdWFyaW8KICAgIHBhcnRpY2lwYW50IEEgYXMgQVBJCiAgICBwYXJ0aWNpcGFudCBLIGFzIEtleWNsb2FrCgogICAgVS0-PkE6IEdFVCAvbG9nb3V0CiAgICBBLT4-SzogUE9TVCAvcmV2b2tlIChyZWZyZXNoX3Rva2VuKQogICAgSy0tPj5BOiAyMDAvMjA0CiAgICBBLS0-PlU6IERlbGV0ZSBjb29raWVzICsgcmVkaXJlY3Q=)

---

## Validación de token

`AuthGuardian` valida el token en cada request consultando a Keycloak en tiempo real mediante `/token/introspect`.

**Comportamiento ante errores:**

| Situación | Respuesta |
|---|---|
| `active: false` | `401 Unauthorized` |
| Keycloak no disponible | `503 Service Unavailable` (sin exponer detalles internos) |
| API de Keycloak caída | Falla introspection aunque la BD de Keycloak esté activa |

---

## API pública estable

Uso habitual:

- `AuthGuardian`
- `create_auth_router`
- `auth.get_current_user`
- `auth.require_role`

---

## Troubleshooting

### `Missing required configuration variables`

**Causa:** Falta una o más variables de entorno obligatorias.

**Solución:** Completar las cuatro variables en tu `.env`:
```env
KEYCLOAK_BASE_URL=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_CLIENT_SECRET=
```

---

### Introspection devuelve 401 o 403

**Causa:** El cliente no está configurado como confidencial en Keycloak, o el secret es incorrecto.

**Solución:**
- Verificar `KEYCLOAK_CLIENT_SECRET` en tu `.env`.
- Confirmar que el cliente tiene **Client authentication** activado en Keycloak.

---

### Login falla en callback (Redirect URI mismatch)

**Causa:** La Redirect URI configurada en Keycloak no coincide con la que usa la app.

**Solución:**
- Revisar la **Valid Redirect URIs** del cliente en Keycloak.
- Si estás detrás de un proxy inverso, verificar que se pasen correctamente las cabeceras `Host` y `X-Forwarded-*`.
