Metadata-Version: 2.4
Name: boolean-auth-sdk
Version: 0.1.0
Summary: SDK to consume cidi-auth and auth.back authentication services from Python apps
Author: Boolean
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31
Requires-Dist: PyJWT[crypto]>=2.8
Requires-Dist: cryptography>=41
Provides-Extra: django
Requires-Dist: Django>=4.2; extra == "django"
Requires-Dist: djangorestframework>=3.14; extra == "django"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-django>=4.7; extra == "dev"
Requires-Dist: responses>=0.24; extra == "dev"

# boolean-auth-sdk (Python) — Integración Django/DRF

SDK para que **juzgado.back** y otros proyectos Django/DRF consuman los servicios de autenticación `cidi-auth` y `auth.back`, que a su vez delegan la firma de tokens en `tokens.back`.

```
📱 Cliente (frontend/backend) ──▶ cidi-auth / auth.back ──▶ tokens.back (.well-known/jwks.json)
```

- `boolean_auth_sdk.AuthClient`: cliente HTTP para login, refresh, logout, `/user/me/`, `/user/by-cuil/`.
- `boolean_auth_sdk.integrations.django_drf.JWTAuthentication`: autenticación DRF lista para usar, con soporte de fallback local y extracción de payload completo.
- `boolean_auth_sdk.JWTVerifier`: verificación manual via JWKS con headers tipo navegador (evita bloqueos de Cloudflare) + fallback local (`PKCS#8` → extrae pública automáticamente).

## Instalación

```bash
# Core + integración Django/DRF
pip install -e "./sdks/python[django]"

# Dependencias de desarrollo (pytest + responses) para correr tests del SDK
pip install -e "./sdks/python[dev]"
```

## 1. Configuración en Django (`settings.py`)

```python
# URL al JWKS que expone tokens.back (mismo que usan auth.back / cidi-auth)
AUTH_SDK_JWKS_URL = "https://tokens.boolean.com.ar/.well-known/jwks.json"

# Algoritmos permitidos (por defecto RS256)
AUTH_SDK_ALGORITHMS = ["RS256"]

# (Opcional) Fallback local: PEM privado o público. Si es privado, se extrae la pública.
AUTH_SDK_LOCAL_SECRET = env.str("JWT_LOCAL_PEM", default=None)

# Prefijos aceptados en Authorization (case-insensitive)
AUTH_SDK_HEADER_PREFIXES = ["token", "bearer"]

# Si el token llega vía cookie HttpOnly
AUTH_SDK_COOKIE_NAME = "access_token"  # opcional

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "boolean_auth_sdk.integrations.django_drf.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
```

- `JWTAuthentication` deja `request.user` como **SDKUser**, con:
  - `user_id`, `username`, `email`, `app_id`, `session_uuid`
  - `roles`: lista de `Role(role_code, role_name)`
  - `payload`: claims completos del JWT
- `JWTRefreshAuthentication` es idéntica pero fuerza `token_type == "refresh"`.

## 2. Views y ViewSets

```python
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

class CasesViewSet(viewsets.ViewSet):
    permission_classes = []  # usa las globales (IsAuthenticated)

    def list(self, request):
        user = request.user  # SDKUser
        return Response({
            "user_id": user.user_id,
            "roles": [r.role_code for r in user.roles],
            "email": user.email,
            "payload": user.payload,
        })

    @action(detail=False, methods=["post"],
            authentication_classes=[JWTRefreshAuthentication])
    def refresh(self, request):
        # Token ya validado como refresh
        return Response({"message": "refresh validado"})
```

- Se puede combinar con cualquier permiso (`IsAuthenticated`, personalizados, etc.).
- `JWTRefreshAuthentication` útil para endpoints que aceptan sólo refresh tokens (ej: reinicio de sesión).

## 3. Mapear a `django.contrib.auth.User`

Si tu app necesita usuarios persistentes (grupos, staff, etc.), subclasá `JWTAuthentication` y sobreescribí `get_user` para sincronizar el payload con tu modelo.

```python
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from boolean_auth_sdk.integrations.django_drf import JWTAuthentication

ROLE_TO_GROUP = {
    "judge": "Juzgado",
    "prosecutor": "Procurador",
    "secretary": "Secretario",
    "auxiliar": "Auxiliar",
    "admin": "admin",
}


class DjangoUserJWTAuthentication(JWTAuthentication):
    def get_user(self, payload, token):
        User = get_user_model()
        user_id = payload.get("user_id") or payload.get("sub")
        email = payload.get("email")
        username = payload.get("username") or email or user_id

        user, _ = User.objects.get_or_create(username=username)
        user.email = email or user.email
        user.first_name = payload.get("first_name") or user.first_name
        user.last_name = payload.get("last_name") or user.last_name
        user.is_active = True
        user.save()

        user.groups.clear()
        for role in payload.get("roles", []):
            code = (role.get("role_code") or role.get("code") or "").lower()
            group_name = ROLE_TO_GROUP.get(code)
            if group_name:
                group, _ = Group.objects.get_or_create(name=group_name)
                user.groups.add(group)

        # Adjuntar payload completo para acceso posterior
        user.payload = payload
        user.roles = payload.get("roles", [])
        user.token = token
        return user
```

En `settings.py` reemplazá el path de la clase:

```python
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "myproject.authentication.DjangoUserJWTAuthentication",
    ],
}
```

## 4. Endpoint de refresh + reemisión de tokens

Ejemplo de endpoint que usa el refresh token del header y reemite llamando a `AuthClient.refresh()`:

```python
from rest_framework.views import APIView
from rest_framework.response import Response
from boolean_auth_sdk import AuthClient
from boolean_auth_sdk.integrations.django_drf import JWTRefreshAuthentication

class RefreshView(APIView):
    authentication_classes = [JWTRefreshAuthentication]
    permission_classes = []  # opcional: IsAuthenticated ya aplicada globalmente

    def post(self, request):
        refresh_token = request.auth  # el string pasado en Authorization
        client = AuthClient(settings.CIDI_AUTH_BASE_URL)
        tokens = client.refresh(refresh_token)
        return Response({
            "access_token": tokens.access_token,
            "refresh_token": tokens.refresh_token,
        })
```

## 5. Consumir los endpoints de auth desde Django

Para llamar a `cidi-auth` o `auth.back` desde tus views/services (por ejemplo, buscar usuarios por CUIL):

```python
from boolean_auth_sdk import AuthClient

client = AuthClient(settings.CIDI_AUTH_BASE_URL, app_id=settings.CIDI_APP_ID)

def buscar_usuario_por_cuil(cuil: str, token: str):
    user = client.find_by_cuil(cuil, access_token=token)
    return user

def info_propia(token: str):
    return client.me(token)
```

> `AuthClient` respeta el timeout (`TOKEN_SIGN_SERVICE_TIMEOUT` en config) y envía User-Agent tipo navegador.

## 6. Autenticación vía cookies

Si el frontend recibe tokens como cookies HttpOnly (`access_token` / `refresh_token`), configurá:

```python
AUTH_SDK_COOKIE_NAME = "access_token"
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [
    "boolean_auth_sdk.integrations.django_drf.JWTAuthentication",
]
```

- Asegurate de configurar `SESSION_COOKIE_SECURE`, `CSRF_COOKIE_SECURE`, `SameSite` según tu despliegue.
- `JWTAuthentication` intentará primero el header Authorization y luego la cookie.

## 7. Migrar desde un autenticador JWT existente

Checklist típico (ej. migración de `authentication/cidi_jwt_authentication.py` en juzgado.back):

1. Agregar `boolean_auth_sdk` a `requirements.txt` / instalar editable.
2. Configurar valores `AUTH_SDK_*` en `settings.py` (los mismos que usan `cidi-auth` / `auth.back`).
3. Reemplazar `DEFAULT_AUTHENTICATION_CLASSES` por la clase del SDK (o subclase custom).
4. Si tu autenticador antiguo re-hidrataba usuarios de Django, copiar la lógica a una subclase de `JWTAuthentication.get_user()`.
5. Actualizar tests para usar el nuevo flujo (o mockear `JWTVerifier`).

## 8. Verificación manual y scripts

```python
from boolean_auth_sdk import JWTVerifier

verifier = JWTVerifier(
    jwks_url=settings.AUTH_SDK_JWKS_URL,
    local_secret=settings.AUTH_SDK_LOCAL_SECRET,
)

payload = verifier.verify(token, expected_token_type="access")
print(payload["user_id"], payload["roles"])
```

- Si `local_secret` es un PEM privado RS*, se extrae la pública automáticamente.
- `TokenExpiredError` / `InvalidTokenError` ayudan a distinguir expirados de inválidos.

## 9. Manejo de errores

| Excepción                    | Cuándo ocurre                                             |
|-----------------------------|-----------------------------------------------------------|
| `AuthenticationError`       | auth back devuelve 401/403 (credenciales inválidas)      |
| `HTTPError`                 | otro status HTTP (500, 404, etc.)                         |
| `InvalidTokenError`         | firma inválida / JWKS no coincide                         |
| `TokenExpiredError`         | el JWT está expirado                                      |

Consejos:

- Revisa que `AUTH_SDK_JWKS_URL` apunte al `.well-known/jwks.json` de `tokens.back` (no al de cidi-auth).
- Si Cloudflare bloquea el JWKS, verifica los headers (el SDK usa un User-Agent tipo Chrome).
- Usá `local_secret` como fallback sólo mientras resuelves problemas de networking.

## 10. Referencia de settings

| Setting                     | Default           | Descripción                                               |
|-----------------------------|-------------------|-----------------------------------------------------------|
| `AUTH_SDK_JWKS_URL`        | `None` (requerido) | URL al JWKS (`https://…/.well-known/jwks.json`)           |
| `AUTH_SDK_ALGORITHMS`      | `["RS256"]`        | Algoritmos aceptados                                      |
| `AUTH_SDK_LOCAL_SECRET`    | `None`            | PEM local (fallback)                                      |
| `AUTH_SDK_AUDIENCE`        | `None`            | Audiencia esperada (si querés validar `aud`)               |
| `AUTH_SDK_HEADER_PREFIXES` | `["token", "bearer"]` | Prefijos aceptados en Authorization                    |
| `AUTH_SDK_COOKIE_NAME`     | `None`            | Nombre de cookie con el access token                      |
| `AUTH_SDK_REQUIRED_TOKEN_TYPE` | `None`        | Forzado en subclases (ej. refresh)                         |

## 11. Tests del SDK

```bash
cd sdks/python
pip install -e ".[dev]"
pytest
```

Usa `responses` + claves RSA realistas para validar JWKS, fallback local y errores.
