---
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.
