fad4006f60
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
451 lines
21 KiB
Markdown
451 lines
21 KiB
Markdown
---
|
|
id: "0010"
|
|
title: "Auth System"
|
|
status: completado
|
|
type: feature
|
|
domain: []
|
|
scope: multi-app
|
|
priority: alta
|
|
depends: []
|
|
blocks: []
|
|
related: []
|
|
created: 2026-05-17
|
|
updated: 2026-05-17
|
|
tags: []
|
|
---
|
|
# 0010 — Auth System
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | 0010 |
|
|
| **Estado** | pendiente |
|
|
| **Prioridad** | alta |
|
|
| **Tipo** | feature |
|
|
|
|
## Dependencias
|
|
|
|
Depende de 0009 (HTTP Server Foundation) para los middleware de auth (`Middleware` type, `http_middleware_chain`, `http_error_response`).
|
|
|
|
---
|
|
|
|
## Objetivo
|
|
|
|
Crear funciones reutilizables de autenticacion y autorizacion en Go (dominio infra) cubriendo JWT, hashing de passwords, sesiones SQLite, cliente OAuth2 y checks RBAC, de forma que cualquier app nueva del registry pueda tener auth completo componiendo primitivas en vez de implementarlo desde cero.
|
|
|
|
## Contexto
|
|
|
|
- Actualmente el registry tiene **CERO funciones de auth genericas**. Solo existe `auth_form_ts_ui` (componente UI de login) y funciones de auth especificas para Metabase (`metabase_auth_go_infra`, `metabase_auth_py_infra`).
|
|
- Existen funciones de password store (`pass_get`, `pass_set`, `pass_generate` en Bash) pero son wrappers de `pass`, no primitivas de hashing/verificacion.
|
|
- `generate_password_bash_cybersecurity` genera passwords pero no los hashea.
|
|
- Cada vez que una app necesita auth (como `sqlite_api` o `deploy_server`), hay que implementar JWT, sesiones y validacion ad-hoc.
|
|
- Con estas funciones, una app nueva solo hace: `password_hash` al registrar, `jwt_generate` al login, `jwt_middleware` en las rutas protegidas, y opcionalmente `rbac_middleware` para permisos granulares.
|
|
- Las funciones HTTP server de 0009 (`Middleware` type, `http_middleware_chain`) son el punto de integracion natural para los middlewares de auth.
|
|
|
|
## Arquitectura
|
|
|
|
```
|
|
functions/infra/
|
|
├── jwt_generate.go — NEW: genera JWT firmado desde claims + secret
|
|
├── jwt_generate.md — NEW
|
|
├── jwt_validate.go — NEW: valida JWT, retorna claims o error
|
|
├── jwt_validate.md — NEW
|
|
├── jwt_middleware.go — NEW: middleware HTTP que valida JWT del header Authorization
|
|
├── jwt_middleware.md — NEW
|
|
├── password_hash.go — NEW: hashea password con bcrypt
|
|
├── password_hash.md — NEW
|
|
├── password_verify.go — NEW: verifica password contra hash bcrypt
|
|
├── password_verify.md — NEW
|
|
├── session_create.go — NEW: crea sesion en SQLite, retorna token
|
|
├── session_create.md — NEW
|
|
├── session_validate.go — NEW: valida token de sesion, retorna datos de usuario
|
|
├── session_validate.md — NEW
|
|
├── session_cleanup.go — NEW: elimina sesiones expiradas
|
|
├── session_cleanup.md — NEW
|
|
├── oauth2_auth_url.go — NEW: construye URL de autorizacion OAuth2
|
|
├── oauth2_auth_url.md — NEW
|
|
├── oauth2_exchange.go — NEW: intercambia auth code por tokens
|
|
├── oauth2_exchange.md — NEW
|
|
├── oauth2_refresh.go — NEW: renueva access token con refresh token
|
|
├── oauth2_refresh.md — NEW
|
|
├── rbac_check.go — NEW: verifica si un rol tiene un permiso (datos puros)
|
|
├── rbac_check.md — NEW
|
|
├── rbac_middleware.go — NEW: middleware HTTP que verifica permisos tras auth
|
|
├── rbac_middleware.md — NEW
|
|
|
|
types/infra/
|
|
├── jwt_claims.md — NEW: metadata del tipo JWTClaims
|
|
├── session.md — NEW: metadata del tipo Session
|
|
├── oauth_config.md — NEW: metadata del tipo OAuthConfig
|
|
├── oauth_tokens.md — NEW: metadata del tipo OAuthTokens
|
|
├── permission.md — NEW: metadata del tipo Permission
|
|
├── role.md — NEW: metadata del tipo Role
|
|
```
|
|
|
|
### Patron pure core / impure shell
|
|
|
|
- **Pure:** `oauth2_auth_url` (construye string URL sin I/O), `rbac_check` (evaluacion de datos en memoria)
|
|
- **Impure:** todo lo demas — JWT usa `time.Now()` para expiracion/validacion, bcrypt hace trabajo criptografico con entropia del OS, sesiones interactuan con SQLite, OAuth2 hace HTTP requests, middlewares interactuan con `http.Request`/`http.ResponseWriter`
|
|
|
|
## Diseno
|
|
|
|
### Tipos
|
|
|
|
```go
|
|
// JWTClaims contiene claims estandar y custom para un JWT
|
|
type JWTClaims struct {
|
|
Subject string `json:"sub"`
|
|
Issuer string `json:"iss,omitempty"`
|
|
Audience string `json:"aud,omitempty"`
|
|
ExpiresAt int64 `json:"exp"`
|
|
IssuedAt int64 `json:"iat"`
|
|
Custom map[string]any `json:"custom,omitempty"`
|
|
}
|
|
|
|
// Session representa una sesion de usuario almacenada en SQLite
|
|
type Session struct {
|
|
Token string `json:"token"`
|
|
UserID string `json:"user_id"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// OAuthConfig contiene la configuracion de un proveedor OAuth2
|
|
type OAuthConfig struct {
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
AuthURL string `json:"auth_url"`
|
|
TokenURL string `json:"token_url"`
|
|
RedirectURL string `json:"redirect_url"`
|
|
Scopes []string `json:"scopes"`
|
|
}
|
|
|
|
// OAuthTokens contiene los tokens obtenidos de un flujo OAuth2
|
|
type OAuthTokens struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
}
|
|
|
|
// Permission representa una accion sobre un recurso
|
|
type Permission struct {
|
|
Resource string `json:"resource"`
|
|
Action string `json:"action"`
|
|
}
|
|
|
|
// Role agrupa permisos bajo un nombre
|
|
type Role struct {
|
|
Name string `json:"name"`
|
|
Permissions []Permission `json:"permissions"`
|
|
}
|
|
```
|
|
|
|
### Funciones
|
|
|
|
| Funcion | Purity | Firma (simplificada) |
|
|
|---------|--------|---------------------|
|
|
| `jwt_generate` | impure | `(claims JWTClaims, secret string) (string, error)` |
|
|
| `jwt_validate` | impure | `(token string, secret string) (JWTClaims, error)` |
|
|
| `jwt_middleware` | impure | `(secret string) Middleware` |
|
|
| `password_hash` | impure | `(password string, cost int) (string, error)` |
|
|
| `password_verify` | impure | `(password string, hash string) error` |
|
|
| `session_create` | impure | `(db *sql.DB, userID string, ttl time.Duration, metadata map[string]any) (Session, error)` |
|
|
| `session_validate` | impure | `(db *sql.DB, token string) (Session, error)` |
|
|
| `session_cleanup` | impure | `(db *sql.DB) (int64, error)` |
|
|
| `oauth2_auth_url` | pure | `(config OAuthConfig, state string) string` |
|
|
| `oauth2_exchange` | impure | `(config OAuthConfig, code string) (OAuthTokens, error)` |
|
|
| `oauth2_refresh` | impure | `(config OAuthConfig, refreshToken string) (OAuthTokens, error)` |
|
|
| `rbac_check` | pure | `(roles []Role, roleName string, perm Permission) bool` |
|
|
| `rbac_middleware` | impure | `(roles []Role, requiredPerm Permission) Middleware` |
|
|
|
|
## Tareas
|
|
|
|
### Fase 1: Tipos
|
|
|
|
- [ ] **1.1** Crear tipo `JWTClaims` en `functions/infra/jwt_claims.go` con `.md` en `types/infra/jwt_claims.md`
|
|
- [ ] **1.2** Crear tipo `Session` en `functions/infra/session.go` con `.md` en `types/infra/session.md`
|
|
- [ ] **1.3** Crear tipo `OAuthConfig` en `functions/infra/oauth_config.go` con `.md` en `types/infra/oauth_config.md`
|
|
- [ ] **1.4** Crear tipo `OAuthTokens` en `functions/infra/oauth_tokens.go` con `.md` en `types/infra/oauth_tokens.md`
|
|
- [ ] **1.5** Crear tipo `Permission` en `functions/infra/permission.go` con `.md` en `types/infra/permission.md`
|
|
- [ ] **1.6** Crear tipo `Role` en `functions/infra/role.go` con `.md` en `types/infra/role.md`
|
|
|
|
### Fase 2: JWT + Password (auth core)
|
|
|
|
- [ ] **2.1** `jwt_generate` — codifica header+payload en base64url, firma con HMAC-SHA256, retorna token string. Setea `iat` automaticamente si no viene en claims.
|
|
- [ ] **2.2** `jwt_validate` — split por `.`, verifica firma HMAC-SHA256, decodifica claims, valida `exp` contra `time.Now()`. Error descriptivo si firma invalida, expirado, o malformado.
|
|
- [ ] **2.3** `password_hash` — wrapper de `golang.org/x/crypto/bcrypt.GenerateFromPassword`. Cost por defecto: 12.
|
|
- [ ] **2.4** `password_verify` — wrapper de `bcrypt.CompareHashAndPassword`. Retorna nil si match, error si no.
|
|
|
|
### Fase 3: Sesiones SQLite
|
|
|
|
- [ ] **3.1** `session_create` — genera token con `crypto/rand` (32 bytes hex), inserta en tabla `sessions` (la funcion crea la tabla si no existe via `CREATE TABLE IF NOT EXISTS`), retorna `Session`.
|
|
- [ ] **3.2** `session_validate` — busca token en tabla `sessions`, verifica que no este expirado, retorna `Session` o error.
|
|
- [ ] **3.3** `session_cleanup` — `DELETE FROM sessions WHERE expires_at < ?` con timestamp actual. Retorna cantidad de filas eliminadas.
|
|
|
|
### Fase 4: OAuth2
|
|
|
|
- [ ] **4.1** `oauth2_auth_url` — construye URL con query params: `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`. Funcion pura, solo concatena strings.
|
|
- [ ] **4.2** `oauth2_exchange` — POST a `token_url` con `grant_type=authorization_code`, `code`, `client_id`, `client_secret`, `redirect_uri`. Parsea JSON response a `OAuthTokens`.
|
|
- [ ] **4.3** `oauth2_refresh` — POST a `token_url` con `grant_type=refresh_token`, `refresh_token`, `client_id`, `client_secret`. Parsea JSON response a `OAuthTokens`.
|
|
|
|
### Fase 5: RBAC + Middlewares
|
|
|
|
- [ ] **5.1** `rbac_check` — busca el rol por nombre en `[]Role`, itera sus permisos, retorna `true` si encuentra match de `resource` + `action`. Funcion pura.
|
|
- [ ] **5.2** `jwt_middleware` — extrae token del header `Authorization: Bearer <token>`, valida con `jwt_validate`, inyecta claims en `r.Context()` con `context.WithValue`, llama `next.ServeHTTP`. Retorna 401 si falta token o es invalido. Usa tipo `Middleware` de 0009.
|
|
- [ ] **5.3** `rbac_middleware` — extrae claims del context (puestas por `jwt_middleware`), lee el rol del campo `custom["role"]`, evalua con `rbac_check`. Retorna 403 si no tiene permiso. Requiere que `jwt_middleware` se ejecute antes en la chain.
|
|
|
|
### Fase 6: Tests y cleanup
|
|
|
|
- [ ] **6.1** Tests para JWT: generar token, validar token valido, rechazar token expirado, rechazar firma invalida
|
|
- [ ] **6.2** Tests para password: hash y verify OK, verify con password incorrecto falla
|
|
- [ ] **6.3** Tests para sesiones: crear, validar, validar expirada, cleanup
|
|
- [ ] **6.4** Tests para OAuth2: `oauth2_auth_url` genera URL correcta (test puro), exchange/refresh con `httptest.NewServer` mock
|
|
- [ ] **6.5** Tests para RBAC: check con permiso, sin permiso, rol inexistente
|
|
- [ ] **6.6** Tests para middlewares: jwt_middleware con token valido/invalido/ausente, rbac_middleware con/sin permiso
|
|
- [ ] **6.7** `fn index` y verificar que todas las funciones y tipos aparecen en registry.db
|
|
- [ ] **6.8** Verificar `go vet -tags fts5` y `go test -tags fts5 ./functions/infra/`
|
|
|
|
---
|
|
|
|
## Ejemplo de uso
|
|
|
|
### Registro de usuario y login con JWT
|
|
|
|
```go
|
|
// Handler de registro
|
|
func registerHandler(w http.ResponseWriter, r *http.Request) {
|
|
var input struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := infra.HttpParseBody(r, &input, 1<<20); err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 400, Code: "bad_request", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
hash, err := infra.PasswordHash(input.Password, 12)
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 500, Code: "hash_error", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
// Guardar email + hash en la BD de la app...
|
|
userID := saveUser(input.Email, hash)
|
|
|
|
infra.HttpJsonResponse(w, 201, map[string]string{"id": userID})
|
|
}
|
|
|
|
// Handler de login
|
|
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
|
var input struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := infra.HttpParseBody(r, &input, 1<<20); err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 400, Code: "bad_request", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
user, err := findUserByEmail(input.Email)
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 401, Code: "invalid_credentials", Message: "email o password incorrectos"})
|
|
return
|
|
}
|
|
|
|
if err := infra.PasswordVerify(input.Password, user.PasswordHash); err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 401, Code: "invalid_credentials", Message: "email o password incorrectos"})
|
|
return
|
|
}
|
|
|
|
claims := infra.JWTClaims{
|
|
Subject: user.ID,
|
|
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
|
|
Custom: map[string]any{"role": "admin", "email": user.Email},
|
|
}
|
|
token, err := infra.JwtGenerate(claims, os.Getenv("JWT_SECRET"))
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 500, Code: "token_error", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
infra.HttpJsonResponse(w, 200, map[string]string{"token": token})
|
|
}
|
|
```
|
|
|
|
### Rutas protegidas con JWT + RBAC
|
|
|
|
```go
|
|
// Definir roles y permisos como datos
|
|
roles := []infra.Role{
|
|
{
|
|
Name: "admin",
|
|
Permissions: []infra.Permission{
|
|
{Resource: "users", Action: "read"},
|
|
{Resource: "users", Action: "write"},
|
|
{Resource: "users", Action: "delete"},
|
|
},
|
|
},
|
|
{
|
|
Name: "viewer",
|
|
Permissions: []infra.Permission{
|
|
{Resource: "users", Action: "read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Rutas publicas
|
|
publicRoutes := []infra.Route{
|
|
{Method: "POST", Path: "/register", Handler: registerHandler},
|
|
{Method: "POST", Path: "/login", Handler: loginHandler},
|
|
}
|
|
|
|
// Rutas protegidas con JWT
|
|
protectedRoutes := []infra.Route{
|
|
{Method: "GET", Path: "/api/me", Handler: meHandler},
|
|
}
|
|
|
|
// Rutas protegidas con JWT + RBAC
|
|
adminRoutes := []infra.Route{
|
|
{Method: "DELETE", Path: "/api/users/{id}", Handler: deleteUserHandler},
|
|
}
|
|
|
|
secret := os.Getenv("JWT_SECRET")
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// Publicas: sin middleware de auth
|
|
for _, r := range publicRoutes {
|
|
mux.HandleFunc(r.Method+" "+r.Path, r.Handler)
|
|
}
|
|
|
|
// Protegidas: JWT middleware
|
|
jwtProtected := infra.HttpMiddlewareChain(
|
|
infra.JwtMiddleware(secret),
|
|
)
|
|
for _, r := range protectedRoutes {
|
|
mux.Handle(r.Method+" "+r.Path, jwtProtected(http.HandlerFunc(r.Handler)))
|
|
}
|
|
|
|
// Admin: JWT + RBAC
|
|
adminProtected := infra.HttpMiddlewareChain(
|
|
infra.JwtMiddleware(secret),
|
|
infra.RbacMiddleware(roles, infra.Permission{Resource: "users", Action: "delete"}),
|
|
)
|
|
for _, r := range adminRoutes {
|
|
mux.Handle(r.Method+" "+r.Path, adminProtected(http.HandlerFunc(r.Handler)))
|
|
}
|
|
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
defer cancel()
|
|
infra.HttpServe(":8080", mux, ctx)
|
|
```
|
|
|
|
### Flujo OAuth2 (ej. Google)
|
|
|
|
```go
|
|
googleConfig := infra.OAuthConfig{
|
|
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
|
|
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
|
|
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
TokenURL: "https://oauth2.googleapis.com/token",
|
|
RedirectURL: "http://localhost:8080/callback",
|
|
Scopes: []string{"openid", "email", "profile"},
|
|
}
|
|
|
|
// 1. Redirigir al usuario al proveedor
|
|
func oauthLoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
state := generateRandomState() // guardar en cookie/session para validar despues
|
|
url := infra.Oauth2AuthUrl(googleConfig, state)
|
|
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
|
}
|
|
|
|
// 2. Callback: intercambiar code por tokens
|
|
func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
code := r.URL.Query().Get("code")
|
|
state := r.URL.Query().Get("state")
|
|
|
|
// Validar state contra el guardado en cookie/session...
|
|
|
|
tokens, err := infra.Oauth2Exchange(googleConfig, code)
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 500, Code: "oauth_error", Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
// Usar tokens.AccessToken para obtener datos del usuario...
|
|
// Crear sesion local o JWT propio
|
|
infra.HttpJsonResponse(w, 200, map[string]string{"access_token": tokens.AccessToken})
|
|
}
|
|
|
|
// 3. Renovar token cuando expire
|
|
func refreshHandler(w http.ResponseWriter, r *http.Request) {
|
|
refreshToken := extractRefreshToken(r)
|
|
newTokens, err := infra.Oauth2Refresh(googleConfig, refreshToken)
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 401, Code: "refresh_failed", Message: err.Error()})
|
|
return
|
|
}
|
|
infra.HttpJsonResponse(w, 200, newTokens)
|
|
}
|
|
```
|
|
|
|
### Sesiones SQLite (alternativa a JWT para apps con estado)
|
|
|
|
```go
|
|
db, _ := sql.Open("sqlite3", "app.db?_journal_mode=wal")
|
|
|
|
// Crear sesion al login
|
|
func sessionLoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
// ... validar credenciales ...
|
|
session, err := infra.SessionCreate(db, user.ID, 24*time.Hour, map[string]any{
|
|
"email": user.Email,
|
|
"role": "admin",
|
|
})
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 500, Code: "session_error", Message: err.Error()})
|
|
return
|
|
}
|
|
infra.HttpJsonResponse(w, 200, map[string]string{"session_token": session.Token})
|
|
}
|
|
|
|
// Validar sesion en cada request
|
|
func protectedHandler(w http.ResponseWriter, r *http.Request) {
|
|
token := r.Header.Get("X-Session-Token")
|
|
session, err := infra.SessionValidate(db, token)
|
|
if err != nil {
|
|
infra.HttpErrorResponse(w, infra.HTTPError{Status: 401, Code: "invalid_session", Message: "sesion invalida o expirada"})
|
|
return
|
|
}
|
|
infra.HttpJsonResponse(w, 200, map[string]any{"user_id": session.UserID, "metadata": session.Metadata})
|
|
}
|
|
|
|
// Limpiar sesiones expiradas periodicamente
|
|
deleted, _ := infra.SessionCleanup(db)
|
|
fmt.Printf("sesiones eliminadas: %d\n", deleted)
|
|
```
|
|
|
|
## Decisiones de diseno
|
|
|
|
- **bcrypt sobre argon2:** bcrypt esta en `golang.org/x/crypto` (dependencia semi-oficial de Go), es robusto y ampliamente adoptado. Argon2 es mas moderno pero agrega complejidad sin beneficio practico para la mayoria de apps del registry. Cost por defecto 12 (buen balance velocidad/seguridad).
|
|
- **JWT con HMAC-SHA256 sin libreria externa:** implementacion manual con `crypto/hmac` + `crypto/sha256` + `encoding/base64`. Solo soporta HS256 — suficiente para apps single-server. Si se necesita RS256 (multiples servicios verificando), se crea una funcion separada en el futuro.
|
|
- **Sesiones en SQLite:** coherente con la filosofia del registry (SQLite en todas partes). La tabla `sessions` se crea en la BD de la app, no en registry.db. `CREATE TABLE IF NOT EXISTS` evita setup manual.
|
|
- **OAuth2 sin `golang.org/x/oauth2`:** las tres operaciones (auth_url, exchange, refresh) son suficientemente simples para implementar con `net/http` y `encoding/json`. Evita una dependencia mas y mantiene el control total del flujo.
|
|
- **RBAC como datos puros:** roles y permisos son slices pasados como argumento, no leidos de BD. Cada app decide donde los almacena (hardcoded, JSON, SQLite). `rbac_check` es pura — la unica funcion pura del modulo junto con `oauth2_auth_url`.
|
|
- **Claims en context con key privada:** `jwt_middleware` inyecta claims en `r.Context()` usando un tipo no exportado como key (`type contextKey struct{}`) para evitar colisiones. `rbac_middleware` las extrae del mismo context.
|
|
|
|
## Prerequisitos
|
|
|
|
- **0009 (HTTP Server Foundation):** tipo `Middleware`, funciones `http_middleware_chain`, `http_error_response`, `http_json_response`, `http_parse_body`. Los middlewares de auth (`jwt_middleware`, `rbac_middleware`) retornan `Middleware` y usan los helpers de response para errores 401/403.
|
|
|
|
## Riesgos
|
|
|
|
- **Seguridad de la implementacion JWT manual:** Mitigado manteniendo el scope en HS256, usando `hmac.Equal` para comparacion constant-time, y validando siempre `exp`. Documentar en cada `.md` que NO es apto para escenarios multi-servicio donde se necesita RSA/ECDSA.
|
|
- **Almacenamiento de secrets:** Las funciones reciben el secret como parametro (`string`), no lo leen de env ni de archivos. Es responsabilidad de la app obtener el secret de forma segura (env var, `pass_get`, etc.). Documentar este patron.
|
|
- **Token en header vs cookie:** `jwt_middleware` solo lee `Authorization: Bearer`. Para apps que necesiten cookies (frontend SSR), se crearia un middleware separado en el futuro. No mezclar ambos patrones en la misma funcion.
|
|
- **SQL injection en sesiones:** Mitigado usando prepared statements con `?` placeholders en todas las queries de sesion. Nunca concatenar strings.
|
|
- **Session fixation:** `session_create` genera tokens con `crypto/rand` (32 bytes = 256 bits de entropia), haciendo inviable la prediccion o fijacion de tokens.
|
|
- **bcrypt timing attacks:** `bcrypt.CompareHashAndPassword` ya es constant-time internamente. No se necesita proteccion adicional.
|