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
+13
View File
@@ -0,0 +1,13 @@
package infra
// JWTClaims contiene claims estandar y custom para un JWT.
// Incluye los campos registrados mas comunes (sub, iss, aud, exp, iat)
// y un mapa libre `Custom` para claims de aplicacion (ej: role, email).
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"`
}
+12
View File
@@ -0,0 +1,12 @@
package infra
// OAuthConfig contiene la configuracion de un proveedor OAuth2.
// Los Scopes se concatenan con espacio al construir la URL de autorizacion.
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"`
}
+10
View File
@@ -0,0 +1,10 @@
package infra
// OAuthTokens contiene los tokens obtenidos de un flujo OAuth2.
// ExpiresAt es Unix epoch seconds calculado a partir de expires_in del proveedor.
type OAuthTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresAt int64 `json:"expires_at"`
}
+8
View File
@@ -0,0 +1,8 @@
package infra
// Permission representa una accion sobre un recurso.
// Ejemplo: {Resource: "users", Action: "delete"}.
type Permission struct {
Resource string `json:"resource"`
Action string `json:"action"`
}
+8
View File
@@ -0,0 +1,8 @@
package infra
// Role agrupa permisos bajo un nombre.
// Ejemplo: {Name: "admin", Permissions: [{Resource:"users", Action:"delete"}, ...]}.
type Role struct {
Name string `json:"name"`
Permissions []Permission `json:"permissions"`
}
+12
View File
@@ -0,0 +1,12 @@
package infra
// Session representa una sesion de usuario almacenada en SQLite.
// Token es un valor aleatorio opaco (32 bytes hex = 64 chars).
// ExpiresAt y CreatedAt son Unix epoch seconds.
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"`
}
+7 -6
View File
@@ -11,8 +11,9 @@ require (
github.com/jackc/pgx/v5 v5.9.1
github.com/marcboeker/go-duckdb v1.8.5
github.com/mattn/go-sqlite3 v1.14.37
golang.org/x/sync v0.19.0
golang.org/x/sync v0.20.0
gopkg.in/yaml.v3 v3.0.1
nhooyr.io/websocket v1.8.17
)
require (
@@ -59,10 +60,10 @@ require (
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
nhooyr.io/websocket v1.8.17 // indirect
)
+12 -10
View File
@@ -159,8 +159,8 @@ golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -170,8 +170,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -180,21 +180,23 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+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 = ?).