auth-guardian 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: auth-guardian
3
+ Version: 0.0.1
4
+ Summary: Reusable Keycloak SDK for auth-service and microservices
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx<0.29,>=0.28
8
+ Requires-Dist: python-jose[cryptography]<4,>=3.5
9
+ Provides-Extra: fastapi
10
+ Requires-Dist: fastapi<0.119,>=0.118; extra == "fastapi"
11
+ Provides-Extra: dev
12
+ Requires-Dist: build<2,>=1.3; extra == "dev"
13
+ Requires-Dist: mypy<2,>=1.10; extra == "dev"
14
+ Requires-Dist: pytest<9,>=8.3; extra == "dev"
15
+ Requires-Dist: pytest-asyncio<2,>=1.2; extra == "dev"
16
+ Requires-Dist: respx<0.23,>=0.22; extra == "dev"
17
+ Requires-Dist: ruff<0.15,>=0.14; extra == "dev"
18
+ Requires-Dist: twine<7,>=6.2; extra == "dev"
19
+
20
+ # Auth Guardian
21
+
22
+ **Auth Guardian** es un SDK extremadamente simple, *plug-and-play* y agnóstico, diseñado para proteger aplicaciones y microservicios en Python utilizando Keycloak (u otros proveedores OIDC compatibles).
23
+
24
+ Ha sido diseñado para evitar configuraciones redundantes: con un par de variables de entorno, tu aplicación tendrá flujos de login, logout, callbacks y validación de roles y permisos automáticamente.
25
+
26
+ ---
27
+
28
+ ## Instalación
29
+
30
+ ```bash
31
+ pip install auth-guardian==0.0.1
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Configuración (Plug-and-Play)
37
+
38
+ `AuthGuardian` es lo suficientemente inteligente como para configurarse por sí mismo. Solo necesitas definir las siguientes variables de entorno:
39
+
40
+ ```env
41
+ AUTH_BASE_URL=https://auth.tu-dominio.com
42
+ AUTH_REALM=tu-realm
43
+ AUTH_CLIENT_ID=tu-cliente
44
+ ```
45
+
46
+ > **Nota:** También soporta las variables legacy `KEYCLOAK_BASE_URL`, `KEYCLOAK_REALM` y `KEYCLOAK_CLIENT_ID` de forma automática.
47
+
48
+ ---
49
+
50
+ ## Uso en FastAPI
51
+
52
+ ### 1. Inyectando Rutas Automáticas (Login, Logout, Callback)
53
+ Para aplicaciones frontend/Fullstack, `AuthGuardian` puede inyectar los endpoints completos del flujo OIDC (`/login`, `/logout`, `/oidc/callback`) para que no tengas que escribirlos:
54
+
55
+ ```python
56
+ from fastapi import FastAPI
57
+ from auth_guardian import AuthGuardian, create_auth_router
58
+
59
+ app = FastAPI()
60
+
61
+ # 1. Instancia el guardián
62
+ auth = AuthGuardian()
63
+
64
+ # 2. Crea el router que manejará automáticamente login, callback y logout
65
+ auth_router = create_auth_router(
66
+ auth=auth,
67
+ oidc_state_secret="tu_secreto_super_seguro_para_estado",
68
+ login_redirect_url="/dashboard", # Dónde enviar al usuario tras loguearse
69
+ logout_redirect_url="/login" # Dónde enviar al usuario tras desloguearse
70
+ )
71
+
72
+ # 3. Registra el router en tu app
73
+ app.include_router(auth_router)
74
+ ```
75
+
76
+ **¡Eso es todo!** Automáticamente tienes:
77
+ - `GET /login` -> Redirige al login de Keycloak.
78
+ - `GET /oidc/callback` -> Maneja el intercambio de código, genera las cookies seguras e inicia la sesión local.
79
+ - `GET /logout` -> Elimina cookies y redirige al cierre de sesión de Keycloak.
80
+
81
+ ### 2. Protegiendo tus Endpoints (Para Principiantes)
82
+ En FastAPI, "proteger" una ruta significa que si alguien entra sin haber iniciado sesión, el sistema le bloqueará el acceso automáticamente.
83
+
84
+ Para lograr esto, FastAPI usa algo llamado `Depends` (Dependencias). Si agregas `Depends(auth.get_current_user)` en tu función, AuthGuardian se encargará de verificar si el usuario tiene sesión activa antes de ejecutar el código.
85
+
86
+ **Ejemplo 1: Bloquear acceso a usuarios no logueados**
87
+ ```python
88
+ from fastapi import Depends
89
+
90
+ @app.get("/recurso-seguro")
91
+ async def get_seguro(user: dict = Depends(auth.get_current_user)):
92
+ # Si llega aquí, es porque el usuario SÍ está logueado.
93
+ # 'user' contendrá la información de su perfil.
94
+ return {
95
+ "mensaje": f"Hola {user.get('preferred_username')}",
96
+ "email": user.get('email')
97
+ }
98
+ ```
99
+
100
+ **Ejemplo 2: Bloquear acceso según el Rol del usuario**
101
+ Si tienes zonas exclusivas (como un panel de administrador), puedes usar `auth.require_role("nombre_del_rol")`.
102
+
103
+ ```python
104
+ @app.get("/zona-admin")
105
+ async def get_admin(user: dict = Depends(auth.require_role("admin"))):
106
+ # Solo entrarán usuarios que se hayan logueado y que tengan el rol "admin"
107
+ return {"mensaje": "¡Bienvenido, administrador!"}
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Opciones Avanzadas
113
+
114
+ ### Validación Manual de Tokens
115
+ Si no usas FastAPI o necesitas validar un token manualmente, el SDK expone internamente el validador:
116
+
117
+ ```python
118
+ payload = await auth.validator.decode_access_token("eyJhbGciOiJIUz...")
119
+ print(payload)
120
+ ```
121
+
122
+ ### Hook Personalizado de Login
123
+ Al usar `create_auth_router`, puedes inyectar un hook que se ejecute justo cuando el usuario hace login exitosamente (por ejemplo, para guardar/actualizar el usuario en tu base de datos):
124
+
125
+ ```python
126
+ async def sync_user_in_db(payload: dict):
127
+ email = payload.get("email")
128
+ print(f"Sincronizando usuario {email} en base de datos...")
129
+
130
+ auth_router = create_auth_router(
131
+ auth=auth,
132
+ oidc_state_secret="secreto",
133
+ on_login_success=sync_user_in_db # Se ejecuta después del callback, antes de redirigir
134
+ )
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Contrato Público API
140
+
141
+ El contrato público exportado es estable y mantenido a través del `__init__.py`.
142
+
143
+ - `AuthGuardian`: Clase principal y Facade de autenticación.
144
+ - `create_auth_router`: Constructor automático de endpoints OIDC para FastAPI.
145
+ - `extract_client_roles(payload, client_id)`: Función helper para extraer los roles.
146
+ - `AuthConfig`, `AuthTokenValidator`, `AuthOIDCClient`: Accesibles para usos complejos o manuales.
147
+ - Manejo de Errores: `KeycloakAuthError`, `TokenValidationError`, `KeycloakAPIError`.
148
+
149
+ ---
150
+
151
+ ## Desarrollo Local
152
+
153
+ ```bash
154
+ pip install .[dev]
155
+ pytest tests/
156
+ ruff check src/ tests/
157
+ mypy src/auth_guardian
158
+ ```
@@ -0,0 +1,139 @@
1
+ # Auth Guardian
2
+
3
+ **Auth Guardian** es un SDK extremadamente simple, *plug-and-play* y agnóstico, diseñado para proteger aplicaciones y microservicios en Python utilizando Keycloak (u otros proveedores OIDC compatibles).
4
+
5
+ Ha sido diseñado para evitar configuraciones redundantes: con un par de variables de entorno, tu aplicación tendrá flujos de login, logout, callbacks y validación de roles y permisos automáticamente.
6
+
7
+ ---
8
+
9
+ ## Instalación
10
+
11
+ ```bash
12
+ pip install auth-guardian==0.0.1
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Configuración (Plug-and-Play)
18
+
19
+ `AuthGuardian` es lo suficientemente inteligente como para configurarse por sí mismo. Solo necesitas definir las siguientes variables de entorno:
20
+
21
+ ```env
22
+ AUTH_BASE_URL=https://auth.tu-dominio.com
23
+ AUTH_REALM=tu-realm
24
+ AUTH_CLIENT_ID=tu-cliente
25
+ ```
26
+
27
+ > **Nota:** También soporta las variables legacy `KEYCLOAK_BASE_URL`, `KEYCLOAK_REALM` y `KEYCLOAK_CLIENT_ID` de forma automática.
28
+
29
+ ---
30
+
31
+ ## Uso en FastAPI
32
+
33
+ ### 1. Inyectando Rutas Automáticas (Login, Logout, Callback)
34
+ Para aplicaciones frontend/Fullstack, `AuthGuardian` puede inyectar los endpoints completos del flujo OIDC (`/login`, `/logout`, `/oidc/callback`) para que no tengas que escribirlos:
35
+
36
+ ```python
37
+ from fastapi import FastAPI
38
+ from auth_guardian import AuthGuardian, create_auth_router
39
+
40
+ app = FastAPI()
41
+
42
+ # 1. Instancia el guardián
43
+ auth = AuthGuardian()
44
+
45
+ # 2. Crea el router que manejará automáticamente login, callback y logout
46
+ auth_router = create_auth_router(
47
+ auth=auth,
48
+ oidc_state_secret="tu_secreto_super_seguro_para_estado",
49
+ login_redirect_url="/dashboard", # Dónde enviar al usuario tras loguearse
50
+ logout_redirect_url="/login" # Dónde enviar al usuario tras desloguearse
51
+ )
52
+
53
+ # 3. Registra el router en tu app
54
+ app.include_router(auth_router)
55
+ ```
56
+
57
+ **¡Eso es todo!** Automáticamente tienes:
58
+ - `GET /login` -> Redirige al login de Keycloak.
59
+ - `GET /oidc/callback` -> Maneja el intercambio de código, genera las cookies seguras e inicia la sesión local.
60
+ - `GET /logout` -> Elimina cookies y redirige al cierre de sesión de Keycloak.
61
+
62
+ ### 2. Protegiendo tus Endpoints (Para Principiantes)
63
+ En FastAPI, "proteger" una ruta significa que si alguien entra sin haber iniciado sesión, el sistema le bloqueará el acceso automáticamente.
64
+
65
+ Para lograr esto, FastAPI usa algo llamado `Depends` (Dependencias). Si agregas `Depends(auth.get_current_user)` en tu función, AuthGuardian se encargará de verificar si el usuario tiene sesión activa antes de ejecutar el código.
66
+
67
+ **Ejemplo 1: Bloquear acceso a usuarios no logueados**
68
+ ```python
69
+ from fastapi import Depends
70
+
71
+ @app.get("/recurso-seguro")
72
+ async def get_seguro(user: dict = Depends(auth.get_current_user)):
73
+ # Si llega aquí, es porque el usuario SÍ está logueado.
74
+ # 'user' contendrá la información de su perfil.
75
+ return {
76
+ "mensaje": f"Hola {user.get('preferred_username')}",
77
+ "email": user.get('email')
78
+ }
79
+ ```
80
+
81
+ **Ejemplo 2: Bloquear acceso según el Rol del usuario**
82
+ Si tienes zonas exclusivas (como un panel de administrador), puedes usar `auth.require_role("nombre_del_rol")`.
83
+
84
+ ```python
85
+ @app.get("/zona-admin")
86
+ async def get_admin(user: dict = Depends(auth.require_role("admin"))):
87
+ # Solo entrarán usuarios que se hayan logueado y que tengan el rol "admin"
88
+ return {"mensaje": "¡Bienvenido, administrador!"}
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Opciones Avanzadas
94
+
95
+ ### Validación Manual de Tokens
96
+ Si no usas FastAPI o necesitas validar un token manualmente, el SDK expone internamente el validador:
97
+
98
+ ```python
99
+ payload = await auth.validator.decode_access_token("eyJhbGciOiJIUz...")
100
+ print(payload)
101
+ ```
102
+
103
+ ### Hook Personalizado de Login
104
+ Al usar `create_auth_router`, puedes inyectar un hook que se ejecute justo cuando el usuario hace login exitosamente (por ejemplo, para guardar/actualizar el usuario en tu base de datos):
105
+
106
+ ```python
107
+ async def sync_user_in_db(payload: dict):
108
+ email = payload.get("email")
109
+ print(f"Sincronizando usuario {email} en base de datos...")
110
+
111
+ auth_router = create_auth_router(
112
+ auth=auth,
113
+ oidc_state_secret="secreto",
114
+ on_login_success=sync_user_in_db # Se ejecuta después del callback, antes de redirigir
115
+ )
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Contrato Público API
121
+
122
+ El contrato público exportado es estable y mantenido a través del `__init__.py`.
123
+
124
+ - `AuthGuardian`: Clase principal y Facade de autenticación.
125
+ - `create_auth_router`: Constructor automático de endpoints OIDC para FastAPI.
126
+ - `extract_client_roles(payload, client_id)`: Función helper para extraer los roles.
127
+ - `AuthConfig`, `AuthTokenValidator`, `AuthOIDCClient`: Accesibles para usos complejos o manuales.
128
+ - Manejo de Errores: `KeycloakAuthError`, `TokenValidationError`, `KeycloakAPIError`.
129
+
130
+ ---
131
+
132
+ ## Desarrollo Local
133
+
134
+ ```bash
135
+ pip install .[dev]
136
+ pytest tests/
137
+ ruff check src/ tests/
138
+ mypy src/auth_guardian
139
+ ```
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "auth-guardian"
7
+ version = "0.0.1"
8
+ description = "Reusable Keycloak SDK for auth-service and microservices"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "httpx>=0.28,<0.29",
13
+ "python-jose[cryptography]>=3.5,<4",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ fastapi = [
18
+ "fastapi>=0.118,<0.119",
19
+ ]
20
+ dev = [
21
+ "build>=1.3,<2",
22
+ "mypy>=1.10,<2",
23
+ "pytest>=8.3,<9",
24
+ "pytest-asyncio>=1.2,<2",
25
+ "respx>=0.22,<0.23",
26
+ "ruff>=0.14,<0.15",
27
+ "twine>=6.2,<7",
28
+ ]
29
+
30
+ [tool.setuptools]
31
+ package-dir = {"" = "src"}
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.pytest.ini_options]
37
+ asyncio_mode = "auto"
38
+ testpaths = ["tests"]
39
+
40
+ [tool.ruff]
41
+ line-length = 100
42
+ target-version = "py310"
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E", "F", "I", "UP", "B"]
46
+
47
+ [tool.ruff.lint.per-file-ignores]
48
+ "src/auth_guardian/fastapi.py" = ["B008"]
49
+
50
+ [tool.mypy]
51
+ python_version = "3.10"
52
+ warn_unused_ignores = true
53
+ warn_redundant_casts = true
54
+ warn_return_any = true
55
+ disallow_untyped_defs = true
56
+ check_untyped_defs = true
57
+ no_implicit_optional = true
58
+
59
+ [[tool.mypy.overrides]]
60
+ module = ["jose", "jose.*", "respx.*", "fastapi", "fastapi.*"]
61
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """Public contract for auth-guardian.
2
+
3
+ Keep this module intentionally small and stable to avoid breaking microservices.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+ from typing import Any
10
+
11
+ from auth_guardian.config import AuthConfig
12
+ from auth_guardian.core import AuthGuardian
13
+ from auth_guardian.exceptions import KeycloakAPIError, KeycloakAuthError, TokenValidationError
14
+ from auth_guardian.oidc_client import AuthOIDCClient
15
+ from auth_guardian.roles import extract_client_roles
16
+ from auth_guardian.router import create_auth_router
17
+ from auth_guardian.state import generate_signed_state, verify_signed_state
18
+ from auth_guardian.token_validator import AuthTokenValidator
19
+
20
+ try:
21
+ __version__ = version("auth-guardian")
22
+ except PackageNotFoundError:
23
+ __version__ = "0.0.0+local"
24
+
25
+ __all__ = [
26
+ "__version__",
27
+ "AuthGuardian",
28
+ "AuthConfig",
29
+ "AuthTokenValidator",
30
+ "AuthOIDCClient",
31
+ "TokenValidationError",
32
+ "KeycloakAuthError",
33
+ "KeycloakAPIError",
34
+ "create_auth_router",
35
+ "extract_client_roles",
36
+ "generate_signed_state",
37
+ "verify_signed_state",
38
+ ]
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class AuthConfig:
6
+ base_url: str
7
+ realm: str
8
+ client_id: str
9
+ client_secret: str | None = None
10
+ issuer: str | None = None
11
+ internal_url: str | None = None
12
+ state_secret: str | None = None
13
+ verify_tls: bool = True
14
+ request_timeout_seconds: float = 20.0
15
+ jwks_cache_ttl_seconds: int = 300
16
+
17
+ @property
18
+ def frontend_realm_url(self) -> str:
19
+ return f"{self.base_url.rstrip('/')}/realms/{self.realm}"
20
+
21
+ @property
22
+ def backend_realm_url(self) -> str:
23
+ base = (self.internal_url or self.base_url).rstrip("/")
24
+ return f"{base}/realms/{self.realm}"
25
+
26
+ @property
27
+ def admin_realm_url(self) -> str:
28
+ base = (self.internal_url or self.base_url).rstrip("/")
29
+ return f"{base}/admin/realms/{self.realm}"
30
+
31
+ @property
32
+ def expected_issuer(self) -> str:
33
+ return self.issuer or self.frontend_realm_url
@@ -0,0 +1,113 @@
1
+ import os
2
+ from collections.abc import Awaitable, Callable
3
+ from typing import Any
4
+
5
+ from fastapi import Depends, HTTPException, Request, Response, Security
6
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
7
+
8
+ from auth_guardian.config import AuthConfig
9
+ from auth_guardian.exceptions import KeycloakAPIError, TokenValidationError
10
+ from auth_guardian.oidc_client import AuthOIDCClient
11
+ from auth_guardian.roles import extract_client_roles
12
+ from auth_guardian.token_validator import AuthTokenValidator
13
+
14
+ security = HTTPBearer(auto_error=False)
15
+
16
+
17
+ class AuthGuardian:
18
+ """
19
+ A simple facade to integrate authentication into FastAPI applications.
20
+ It automatically reads from environment variables if not explicitly provided.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ base_url: str | None = None,
26
+ realm: str | None = None,
27
+ client_id: str | None = None,
28
+ state_secret: str | None = None,
29
+ **kwargs: Any,
30
+ ):
31
+ base_url = base_url or os.getenv("AUTH_BASE_URL") or os.getenv("KEYCLOAK_BASE_URL") or ""
32
+ realm = realm or os.getenv("AUTH_REALM") or os.getenv("KEYCLOAK_REALM") or ""
33
+ client_id = client_id or os.getenv("AUTH_CLIENT_ID") or os.getenv("KEYCLOAK_CLIENT_ID") or ""
34
+ state_secret = state_secret or os.getenv("AUTH_STATE_SECRET") or os.getenv("KEYCLOAK_STATE_SECRET")
35
+
36
+ self.config = AuthConfig(
37
+ base_url=base_url,
38
+ realm=realm,
39
+ client_id=client_id,
40
+ state_secret=state_secret,
41
+ **kwargs,
42
+ )
43
+ self.validator = AuthTokenValidator(self.config)
44
+ self.oidc_client = AuthOIDCClient(self.config)
45
+
46
+ async def get_current_user(
47
+ self,
48
+ request: Request,
49
+ response: Response,
50
+ credentials: HTTPAuthorizationCredentials | None = Security(security),
51
+ ) -> dict[str, Any]:
52
+ """
53
+ FastAPI dependency to get the authenticated user's token payload.
54
+ Handles silent token refreshing if the token is expired and a valid refresh token exists.
55
+ """
56
+ token = credentials.credentials if credentials else request.cookies.get("access_token")
57
+ cookie_name = "access_token"
58
+ if not token:
59
+ token = request.cookies.get("pfx_token")
60
+ if token:
61
+ cookie_name = "pfx_token"
62
+
63
+ if not token:
64
+ raise HTTPException(status_code=401, detail="Not authenticated")
65
+
66
+ try:
67
+ return await self.validator.decode_access_token(token)
68
+ except TokenValidationError as exc:
69
+ if "expirado" in exc.detail.lower() or "expired" in exc.detail.lower():
70
+ refresh_token = request.cookies.get("refresh_token")
71
+ if not refresh_token:
72
+ refresh_token = request.cookies.get("pfx_refresh_token")
73
+
74
+ if refresh_token:
75
+ try:
76
+ token_data = await self.oidc_client.refresh_access_token(refresh_token)
77
+ new_access_token = token_data["access_token"]
78
+ new_refresh_token = token_data.get("refresh_token", refresh_token)
79
+
80
+ # Set new cookies on the response object
81
+ is_prod = os.getenv("IS_PROD", "false").lower() == "true"
82
+ response.set_cookie(
83
+ cookie_name,
84
+ new_access_token,
85
+ httponly=True,
86
+ samesite="lax",
87
+ secure=is_prod,
88
+ )
89
+ response.set_cookie(
90
+ "refresh_token" if not request.cookies.get("pfx_refresh_token") else "pfx_refresh_token",
91
+ new_refresh_token,
92
+ httponly=True,
93
+ samesite="lax",
94
+ secure=is_prod,
95
+ )
96
+
97
+ return await self.validator.decode_access_token(new_access_token)
98
+ except (KeycloakAPIError, TokenValidationError):
99
+ pass
100
+
101
+ raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc
102
+
103
+ def require_role(self, *required_roles: str) -> Callable[..., Awaitable[dict[str, Any]]]:
104
+ """
105
+ FastAPI dependency factory to require specific roles.
106
+ """
107
+ async def _role_guard(user: dict[str, Any] = Depends(self.get_current_user)) -> dict[str, Any]:
108
+ roles = set(extract_client_roles(user, self.config.client_id))
109
+ if not roles.intersection(required_roles):
110
+ raise HTTPException(status_code=403, detail="Insufficient permissions")
111
+ return user
112
+
113
+ return _role_guard
@@ -0,0 +1,16 @@
1
+ class KeycloakAuthError(Exception):
2
+ def __init__(self, detail: str, status_code: int = 401):
3
+ super().__init__(detail)
4
+ self.detail = detail
5
+ self.status_code = status_code
6
+
7
+
8
+ class TokenValidationError(KeycloakAuthError):
9
+ pass
10
+
11
+
12
+ class KeycloakAPIError(Exception):
13
+ def __init__(self, detail: str, status_code: int = 502):
14
+ super().__init__(detail)
15
+ self.detail = detail
16
+ self.status_code = status_code