feat: tipos auth (JWTClaims, Session, OAuthConfig, OAuthTokens, Permission, Role)

Fase 1 del issue 0010 — tipos base del sistema de auth en dominio infra.
Define las estructuras que usaran jwt_*, session_*, oauth2_* y rbac_*.

Añade dep golang.org/x/crypto/bcrypt para el hashing de passwords.
This commit is contained in:
2026-04-18 17:37:19 +02:00
parent a5d532c001
commit 1aab74467b
14 changed files with 295 additions and 16 deletions
+39
View File
@@ -0,0 +1,39 @@
---
name: JWTClaims
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
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"`
}
description: "Claims de un JSON Web Token. Incluye los campos registrados (sub, iss, aud, exp, iat) y un mapa Custom libre para claims de aplicacion como role o email."
tags: [jwt, auth, token, claims, infra]
uses_types: []
file_path: "functions/infra/jwt_claims.go"
---
## Ejemplo
```go
claims := JWTClaims{
Subject: "user-123",
Issuer: "my-api",
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
Custom: map[string]any{
"role": "admin",
"email": "alice@example.com",
},
}
token, _ := JWTGenerate(claims, secret)
```
## Notas
Tipo producto — los campos estandar cubren RFC 7519. ExpiresAt y IssuedAt son Unix epoch seconds. JWTGenerate setea IssuedAt automaticamente si viene en cero. Custom se serializa bajo la clave "custom" en el payload JSON para evitar colisiones con claims registrados. Para leer valores custom de forma segura tras JWTValidate: `v, ok := claims.Custom["role"].(string)`.
+38
View File
@@ -0,0 +1,38 @@
---
name: OAuthConfig
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
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"`
}
description: "Configuracion de un proveedor OAuth2. Contiene credenciales del cliente, endpoints de autorizacion/token, redirect URI y scopes solicitados."
tags: [oauth, oauth2, auth, config, infra]
uses_types: []
file_path: "functions/infra/oauth_config.go"
---
## Ejemplo
```go
google := 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"},
}
url := Oauth2AuthURL(google, "random-state-123")
```
## Notas
Tipo producto — agrupa todo lo necesario para los tres flujos OAuth2 soportados (auth URL, code exchange, refresh). ClientSecret nunca debe salir del servidor: Oauth2Exchange y Oauth2Refresh lo envian al TokenURL del proveedor, no al cliente. Scopes se concatenan con espacio al construir la URL (`openid email profile`).
+32
View File
@@ -0,0 +1,32 @@
---
name: OAuthTokens
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type OAuthTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresAt int64 `json:"expires_at"`
}
description: "Tokens OAuth2 obtenidos de un flujo de autorizacion. AccessToken es el de corta vida para llamadas a APIs. RefreshToken renueva el access. ExpiresAt es Unix epoch seconds."
tags: [oauth, oauth2, token, infra]
uses_types: []
file_path: "functions/infra/oauth_tokens.go"
---
## Ejemplo
```go
tokens, _ := Oauth2Exchange(googleConfig, code)
if time.Now().Unix() >= tokens.ExpiresAt {
tokens, _ = Oauth2Refresh(googleConfig, tokens.RefreshToken)
}
req.Header.Set("Authorization", tokens.TokenType + " " + tokens.AccessToken)
```
## Notas
Tipo producto — producto del flujo OAuth2. AccessToken tipicamente expira en 1h. RefreshToken puede durar dias/meses segun el proveedor. TokenType suele ser "Bearer". ExpiresAt se calcula como `time.Now().Unix() + expires_in` al parsear la respuesta del proveedor, asi el consumidor solo compara con `time.Now().Unix()` para saber si renovar.
+29
View File
@@ -0,0 +1,29 @@
---
name: Permission
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type Permission struct {
Resource string `json:"resource"`
Action string `json:"action"`
}
description: "Permiso RBAC: accion sobre un recurso. Par (resource, action) evaluado por RBACCheck contra los permisos de un rol."
tags: [rbac, permission, auth, infra]
uses_types: []
file_path: "functions/infra/permission.go"
---
## Ejemplo
```go
p := Permission{Resource: "users", Action: "delete"}
if RBACCheck(roles, "admin", p) {
// el rol admin puede borrar usuarios
}
```
## Notas
Tipo producto — Resource y Action son strings libres, decididos por la app. Convencion: Resource en plural snake_case (`users`, `articles`, `billing_invoices`), Action verbo minusculas (`read`, `write`, `delete`, `admin`). Los wildcards `*` no se interpretan — si quieres "todas las acciones" define una Permission explicita por cada una en el rol, o crea un rol superadmin fuera del sistema RBAC.
+41
View File
@@ -0,0 +1,41 @@
---
name: Role
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type Role struct {
Name string `json:"name"`
Permissions []Permission `json:"permissions"`
}
description: "Rol RBAC con nombre y lista de permisos. Los roles son datos puros — cada app decide donde los almacena (hardcoded, JSON, SQLite)."
tags: [rbac, role, auth, infra]
uses_types: [Permission_go_infra]
file_path: "functions/infra/role.go"
---
## Ejemplo
```go
roles := []Role{
{
Name: "admin",
Permissions: []Permission{
{Resource: "users", Action: "read"},
{Resource: "users", Action: "write"},
{Resource: "users", Action: "delete"},
},
},
{
Name: "viewer",
Permissions: []Permission{
{Resource: "users", Action: "read"},
},
},
}
```
## Notas
Tipo producto — Name identifica el rol por nombre (coincide con el valor de `claims.Custom["role"]` para RBACMiddleware). Permissions es la lista completa de acciones que el rol puede realizar. No hay herencia entre roles: si admin debe tener todo lo de viewer, hay que incluir esos permisos explicitamente en la definicion de admin. Esta es una decision consciente para mantener la evaluacion lineal y predecible.
+34
View File
@@ -0,0 +1,34 @@
---
name: Session
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
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"`
}
description: "Sesion de usuario almacenada en SQLite. Alternativa server-side a JWT para apps con estado. Token es 32 bytes hex opacos generados con crypto/rand."
tags: [session, auth, sqlite, token, infra]
uses_types: []
file_path: "functions/infra/session.go"
---
## Ejemplo
```go
session, _ := SessionCreate(db, "user-123", 24*time.Hour, map[string]any{
"role": "admin",
"email": "alice@example.com",
})
// session.Token es el valor opaco a enviar al cliente
w.Header().Set("X-Session-Token", session.Token)
```
## Notas
Tipo producto — Token identifica la sesion en la tabla SQLite. UserID vincula la sesion al usuario. ExpiresAt y CreatedAt son Unix epoch seconds. Metadata es un mapa libre que se serializa a JSON en la columna metadata de la tabla sessions. A diferencia de JWT, Session requiere una query a la BD para validar en cada request — a cambio permite invalidacion inmediata (DELETE FROM sessions WHERE token = ?).