🛡️ Autenticación¶
La autenticación identifica al cliente que está haciendo el request (login) y verifica que tiene permiso para acceder a un recurso concreto (autorización por rol/operación).
En HIX hay dos pilas que cubren los dos grandes escenarios:
| Pila | Estado | Uso típico |
|---|---|---|
| Sesión + middleware de auth | statefull (cookie + storage servidor) | App web tradicional, panel admin |
| JWT | stateless (token firmado) | API REST, móvil, microservicios |
Esta página cubre la pila session-based - el patrón que usa Fenix. Para la pila JWT consulta JWT.
Piezas del puzzle¶
┌──────────────────────────────────────────────────────────────┐
│ HIX_MwSession carga/crea la sesión (cookie HIXSID) │
│ │ │
│ ▼ │
│ HIX_MwIsAuth lee session["_auth_user"] │
│ - existe -> hData["user"] := hUser │
│ - NIL -> 302 /login │
│ │ │
│ ▼ │
│ HIX_MwHasRole compara oCtx:cScope con user["roles"] │
│ - permite -> continúa │
│ - rechaza -> 403 │
│ │ │
│ ▼ │
│ Controller hData["user"] disponible vía URequest() │
└──────────────────────────────────────────────────────────────┘
Las tres piezas son middlewares independientes que se combinan en una sola cadena reusable.
Setup¶
Convención Fenix - www/middlewares/config.json¶
{
"setup": {
"auth": {
"session_user_key": "_auth_user",
"roles_key": "roles",
"redirect_login": "/login",
"redirect_accept": "/main"
},
"session": {
"cookie": "FENIXSID",
"ttl": 3600,
"storage": "memory"
}
}
}
| Clave | Para qué sirve |
|---|---|
session_user_key |
Clave dentro del hash de sesión donde se guarda el usuario |
roles_key |
Clave dentro del hash de usuario que contiene los roles |
redirect_login |
URL a la que redirigir si no hay sesión activa (302) |
redirect_accept |
URL post-login OK |
Esos valores se leen con UMwConfig( "auth", "session_user_key" ) desde
cualquier controller o middleware.
Definir los grupos de middleware¶
En Fenix se definen una sola vez y se reutilizan en todas las rutas:
// www/middlewares/myappauth.prg
FUNCTION MyAppAuth( oCtx )
LOCAL o := UBaseMiddleware():New( oCtx )
o:Add( UMiddleware():New( "HIX_MwSession" ) )
o:Add( UMiddleware():New( "HIX_MwIsAuth" ) )
RETURN o:Run()
FUNCTION MyAppAuthRole( oCtx )
LOCAL o := UBaseMiddleware():New( oCtx )
o:Add( UMiddleware():New( "HIX_MwSession" ) )
o:Add( UMiddleware():New( "HIX_MwIsAuth" ) )
o:Add( UMiddleware():New( "HIX_MwHasRole" ) )
RETURN o:Run()
Y en el config.json se cargan al arranque:
Aplicar a las rutas¶
[
{ "name": "main",
"url": "/main", "method": "GET",
"action": "controllers/main.prg",
"middleware": "MyAppAuth" },
{ "name": "customer.show",
"url": "/customer/:id", "method": "GET",
"action": "controllers/masters/show@customer.prg",
"middleware": "MyAppAuthRole", "scope": "customers:show" },
{ "name": "customer.delete",
"url": "/customer/:id([0-9]+)/delete", "method": "POST",
"action": "controllers/masters/delete@customer.prg",
"middleware": "MyAppAuthRole", "scope": "customers:delete" }
]
El campo
"scope"se mapea aoCtx:cScopey lo consumeHIX_MwHasRole.
El hash de usuario¶
HIX_MwIsAuth espera que la sesión contenga un hash con esta forma:
{ "id" => "1",
"name" => "Admin Demo",
"roles" => { "customers" => "show;edit;delete;create",
"sales" => "",
"purchases" => "" } }
Reglas del hash roles:
| Valor | Significado |
|---|---|
"" |
El usuario tiene acceso total a ese rol (cualquier operación pasa) |
"op1;op2;op3" |
Solo esas operaciones están permitidas |
| (rol ausente) | El usuario no tiene ese rol → 403 |
Sintaxis del scope¶
oCtx:cScope se compara contra el hash de roles del usuario:
| Scope | Pasa si... |
|---|---|
"" |
Siempre (sin chequeo de rol) |
"customers" |
El usuario tiene el rol customers (con cualquier ops) |
"customers:show" |
El usuario tiene customers con "" o con "show" dentro de la lista |
"customers:delete" |
El usuario tiene customers con "" o con "delete" en la lista |
// Lectura en HIX_MwHasRole
aScope := hb_ATokens( oCtx:cScope, ":" )
cRole := aScope[1] // "customers"
cOp := iif( Len( aScope ) > 1, aScope[2], "") // "show"
cOps := hb_HGetDef( hRoles, cRole, NIL )
Login - auth.prg de Fenix¶
// POST /auth middleware: MyAppLogin (Session + CsrfCheck)
FUNCTION Main()
LOCAL oVal, oSess, hUser
oVal := UValidatePost( { ;
"username" => { "required|min:3|max:30", "Username", "" }, ;
"password" => { "required|min:4", "Password", "" } ;
} )
IF ! oVal:Make()
UFlash( "login" ):Set( { ;
"error" => oVal:GetFirstError(), ;
"user" => oVal:Get( "username" ) } )
URedirect( "/login" )
RETURN
ENDIF
hUser := ModelUser( oVal:Get( "username" ), oVal:Get( "password" ) )
IF ValType( hUser ) == "H"
oSess := USession()
oSess:Set( UMwConfig( "auth", "session_user_key" ), hUser )
oSess:Save()
URedirect( UMwConfig( "auth", "redirect_accept" ) ) // -> /main
ELSE
UFlash( "login" ):Set( { ;
"error" => "Usuario o contrasena incorrectos", ;
"user" => oVal:Get( "username" ) } )
URedirect( UMwConfig( "auth", "redirect_login" ) ) // -> /login
ENDIF
RETURN
#include '/models/modeluser.prg'
Modelo de usuario - modeluser.prg¶
FUNCTION ModelUser( cUser, cPass )
LOCAL hStore, hEntry
hStore := { ;
"demo" => { "id" => "1", "name" => "Admin Demo", "pass" => "1234", ;
"roles" => { "sales" => "", ;
"purchases" => "", ;
"customers" => "show;edit;delete;create" } ;
}, ;
"carles" => { "id" => "2", "name" => "Carles Aubia", "pass" => "1234", ;
"roles" => { "customers" => "show", ;
"purchases" => "" } } ;
}
hEntry := hb_HGetDef( hStore, Lower( cUser ), NIL )
IF hEntry == NIL .OR. ! ( hEntry["pass"] == cPass )
RETURN NIL
ENDIF
RETURN { "id" => hEntry["id"], ;
"name" => hEntry["name"], ;
"roles" => hEntry["roles"] }
En producción
ModelUserconsulta una BD y compara conhb_BCrypto similar. La forma del hash devuelto no cambia.
Logout - logout.prg de Fenix¶
Destroy() borra todo el contenido de la sesión y emite una cookie con
Max-Age=0 para que el navegador la elimine.
Leer al usuario desde un controller¶
HIX_MwIsAuth deja el hash en oCtx:hData["user"] y en
oReq:hData["user"], así que cualquier controller protegido puede leerlo
sin tocar la sesión directamente:
PROCEDURE Main(...)
LOCAL oReq := URequest()
LOCAL hUser := hb_HGetDef( oReq:hData, "user", { "name" => "Unknown" } )
RETURN UView( "main.view.html", hUser["name"], hUser )
Chequear roles en código¶
IF UHasRole( "customers" ) // tiene el rol (cualquier op)
IF UHasRole( "customers", "delete" ) // tiene el rol con esa op
Cuándo usar qué pila¶
| Caso de uso | Pila |
|---|---|
| App web con login form | ✅ Session + IsAuth |
| Panel admin con permisos finos | ✅ Session + IsAuth + HasRole |
| API REST consumida por SPA con cookie | ✅ Session |
| API REST stateless / móvil / microservicios | ❌ Usa JWT |
| Mismo endpoint accesible desde web + API | Combina ambos middlewares |
Buenas prácticas¶
- Una sola fuente de verdad de roles. El hash
rolesviaja en la sesión - si cambias los permisos del usuario en BD, refresca la sesión o invalídala. Destroy()en logout. Borra toda la sesión, no solo la clave de usuario, para evitar session fixation.- CSRF obligatorio en formularios POST de auth. El login redirige por
URedirecty deja la sesión iniciada - sin CSRF, un atacante puede forzar el login. Combina con CSRF. - HTTPS en producción. La cookie viaja en cada request - sin SSL un intermediario la roba.
- No metas el password en la sesión. Solo
id,name,rolesy lo estrictamente necesario para autorizar requests. - Roles granulares con
""para admin. El valor vacío ("customers" => "") significa "todas las ops" - úsalo solo para superusers.