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

RolDescripciónPermisos
adminAdministrador del sistemaCRUD total, gestión de usuarios, configuración
editorEditor de contenidoCrear, editar, leer
viewerSolo lecturaLeer 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)?

Referencias