Autenticación y Roles
Sistema de autenticación SSO Google, roles RBAC y manejo de sesiones para aplicaciones del DTI.
Autenticación — SSO institucional Google @unach.cl y sistema de roles
Propósito
Todas las aplicaciones del DTI deben usar el sistema de autenticación SSO de Google para usuarios institucionales. Esto garantiza seguridad centralizada y facilita la gestión de usuarios.
Autenticación SSO Google
Flujo OAuth/OIDC
Usuario → click "Iniciar sesión" → Google OAuth → Callback con code
→ Intercambiar code por tokens → Verificar email en lista allowed → Crear/actualizar usuario
Implementación FastAPI
# app/core/auth/google.py
from google.auth.transport import requests
from google.oauth2 import id_token
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
ALLOWED_EMAILS = os.getenv("ALLOWED_DTI_EMAILS", "").split(",")
class GoogleAuth:
@staticmethod
async def verify_token(token: str) -> dict:
try:
idinfo = id_token.verify_oauth2_token(
token,
requests.Request(),
GOOGLE_CLIENT_ID
)
return idinfo
except ValueError:
raise InvalidTokenError()
@staticmethod
def is_allowed_email(email: str) -> bool:
return email.strip() in ALLOWED_EMAILS
Implementación NestJS
// src/modules/auth/google.service.ts
import { Injectable } from '@nestjs/common';
import { OAuth2Client } from 'google-auth-library';
@Injectable()
export class GoogleAuthService {
private client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
private allowedEmails = process.env.ALLOWED_DTI_EMAILS?.split(',') || [];
async verify(token: string) {
const ticket = await this.client.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_CLIENT_ID,
});
return ticket.getPayload();
}
isAllowedEmail(email: string): boolean {
return this.allowedEmails.includes(email.trim());
}
}
Lista de usuarios permitidos
No cualquier usuario @unach.cl debe tener acceso. Solo usuarios específicos del equipo DTI.
Variable de entorno: ALLOWED_DTI_EMAILS
# .env
ALLOWED_DTI_EMAILS=[email protected],[email protected],[email protected]
Validación en login:
# app/api/auth.py
@router.get("/auth/callback")
async def google_callback(request: Request):
google_user = await auth_service.verify_google_token(code)
if not auth_service.is_allowed_email(google_user.email):
raise HTTPException(403, "Usuario no autorizado para acceder a este sistema")
# Continuar con login normal
user = upsert_user(google_user)
return create_session(user)
Sistema de Roles (RBAC)
Roles predefinidos
| Rol | Descripción | Permisos |
|---|---|---|
admin | Administrador del sistema | CRUD total, gestión de usuarios, configuración |
editor | Editor de contenido | Crear, editar, leer |
viewer | Solo lectura | Leer contenido público |
Implementación Django
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
ROLE_CHOICES = [
('admin', 'Administrador'),
('editor', 'Editor'),
('viewer', 'Espectador'),
]
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='viewer')
email = models.EmailField(unique=True)
# Decorador para verificar rol
from functools import wraps
from django.http import JsonResponse
def role_required(role):
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if request.user.role != role and request.user.role != 'admin':
return JsonResponse({'error': 'No autorizado'}, status=403)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
Implementación FastAPI
# app/core/security/roles.py
from enum import Enum
from fastapi import Depends, HTTPException
from app.models.user import User, UserRole
class PermissionChecker:
def __init__(self, allowed_roles: list[UserRole]):
self.allowed_roles = allowed_roles
def __call__(self, user: User = Depends(get_current_user)):
if user.role not in self.allowed_roles and user.role != UserRole.ADMIN:
raise HTTPException(403, "No tienes permisos para esta acción")
return user
# Uso en endpoints
@router.delete("/users/{user_id}")
def delete_user(
user_id: int,
_: User = Depends(PermissionChecker([UserRole.ADMIN]))
):
# lógica de eliminación
pass
Implementación Angular
// src/app/core/guards/role.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
return () => {
const auth = inject(AuthService);
const router = inject(Router);
if (allowedRoles.includes(auth.currentUser.role)) {
return true;
}
return router.createUrlTree(['/unauthorized']);
};
};
// Uso en routing
export const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [roleGuard(['admin'])]
}
];
Tokens JWT
Acceso y Refresh
# FastAPI - generación de tokens
from datetime import datetime, timedelta
from jose import jwt
SECRET_KEY = os.getenv("JWT_SECRET")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE = 30 # minutos
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def create_refresh_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(days=7)
to_encode.update({"exp": expire, "type": "refresh"})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
Almacenamiento seguro
- Access token: puede estar en memoria (no localStorage)
- Refresh token: HTTP-only cookie, SameSite=Strict
# FastAPI - refresh token en cookie
from fastapi import Response
@router.post("/auth/refresh")
def refresh(response: Response, refresh_token: str = Cookie(None)):
if not refresh_token:
raise HTTPException(401, "Refresh token requerido")
# verificar y devolver nuevo access token
response.set_cookie(
key="refresh_token",
value=new_refresh_token,
httponly=True,
samesite="strict",
secure=True # solo en HTTPS
)
Logout y cleanup
@router.post("/auth/logout")
def logout(response: Response, _: User = Depends(get_current_user)):
response.delete_cookie("refresh_token")
return {"message": "Sesión cerrada"}
Implementación en Angular (Auth Service)
// src/app/core/services/auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
loginWithGoogle() {
window.location.href = '/api/auth/google';
}
logout() {
return this.http.post('/api/auth/logout', {});
}
get currentUser(): User | null {
// Desde token decodificado o幸福的
return this.userSubject.value;
}
}
Checklist de auth
- ¿Se verifica que el email esté en la lista
ALLOWED_DTI_EMAILS? - ¿La lista de emails permitidos es configurable por variable de entorno?
- ¿Se usa JWT para sesiones?
- ¿El refresh token está en HTTP-only cookie?
- ¿Los roles se verifican en cada endpoint protegido?
- ¿Hay logout que limpia tokens?
- ¿Rate limiting en login (máx 5 intentos por IP)?