Saltar a contenido

Table of Contents generated with DocToc

Investigacion: Firebase Admin SDK - Verificacion de Tokens y Gestion de Usuarios

Identificador: MTS-INV-016 Version: 1.0.0 Fecha: 2025-12-07 Autor: SpecQueen + SecurityDrone Solicitado en: IT-02 (Seguridad Servidor) Estado: Completado


1. Resumen Ejecutivo

Investigacion tecnica del Firebase Admin SDK para implementacion server-side en MedTime. Cubre verificacion de ID tokens, creacion de custom tokens, gestion de usuarios, y mejores practicas de seguridad.

1.1. Conclusion Principal

Aspecto Evaluacion Notas
Verificacion JWT Nativa Metodo verify_id_token()
Custom Claims Soportado Hasta 1000 bytes
Gestion Usuarios Completa CRUD sin rate limiting
Python SDK Oficial firebase-admin package
Setup Sencillo Service account JSON

2. Descripcion del Admin SDK

2.1. Que es Firebase Admin SDK

El Firebase Admin SDK proporciona acceso privilegiado a los servicios de Firebase desde entornos de servidor. Permite operaciones administrativas sin las restricciones de los SDKs cliente.

Caracteristica Descripcion
Privilegios Acceso completo a proyecto Firebase
Rate Limiting No aplica (a diferencia de cliente)
Autenticacion Via service account
Lenguajes Node.js, Python, Go, Java, C#

2.2. Capacidades Principales

Capacidades Admin SDK:
  authentication:
    - Verificar ID tokens
    - Crear custom tokens
    - Gestionar usuarios (CRUD)
    - Asignar custom claims
    - Revocar tokens

  firestore:
    - Acceso completo a documentos
    - Bypass de security rules

  realtime_database:
    - Acceso administrativo completo

  cloud_messaging:
    - Enviar notificaciones push

  cloud_storage:
    - Generar URLs firmadas

3. Instalacion y Configuracion

3.1. Instalacion Python

pip install firebase-admin

3.2. Service Account

El Admin SDK requiere un service account para autenticarse:

Service Account Setup:
  1_crear:
    - Firebase Console > Project Settings > Service Accounts
    - Generar nueva clave privada
    - Descargar JSON

  2_contenido_json:
    project_id: "medtime-prod"
    private_key_id: "abc123..."
    private_key: "-----BEGIN PRIVATE KEY-----\n..."
    client_email: "firebase-adminsdk-xxx@medtime-prod.iam.gserviceaccount.com"
    client_id: "123456789"
    # ... otros campos

  3_seguridad:
    - NUNCA commitear a git
    - Almacenar en Secret Manager
    - Rotar periodicamente

3.3. Inicializacion

import firebase_admin
from firebase_admin import credentials, auth

# Opcion 1: Archivo JSON (desarrollo)
cred = credentials.Certificate("path/to/service-account.json")
firebase_admin.initialize_app(cred)

# Opcion 2: Variable de entorno (produccion)
# GOOGLE_APPLICATION_CREDENTIALS=path/to/service-account.json
firebase_admin.initialize_app()

# Opcion 3: Contenido JSON desde Secret Manager
import json
from google.cloud import secretmanager

client = secretmanager.SecretManagerServiceClient()
name = "projects/medtime-prod/secrets/firebase-admin/versions/latest"
response = client.access_secret_version(request={"name": name})
cred_dict = json.loads(response.payload.data.decode("UTF-8"))
cred = credentials.Certificate(cred_dict)
firebase_admin.initialize_app(cred)

4. Verificacion de ID Tokens

4.1. Metodo Basico

from firebase_admin import auth

def verify_id_token(id_token: str) -> dict:
    """Verifica un ID token de Firebase.

    Args:
        id_token: JWT enviado por el cliente

    Returns:
        dict: Claims decodificados del token

    Raises:
        auth.InvalidIdTokenError: Token malformado
        auth.ExpiredIdTokenError: Token expirado
        auth.RevokedIdTokenError: Token revocado
    """
    decoded_token = auth.verify_id_token(id_token)
    return decoded_token

4.2. Verificacion con Check de Revocacion

def verify_id_token_with_revocation_check(id_token: str) -> dict:
    """Verifica token incluyendo chequeo de revocacion.

    NOTA: Agrega latencia adicional (llamada a Firebase).
    Usar solo para operaciones sensibles.
    """
    decoded_token = auth.verify_id_token(
        id_token,
        check_revoked=True  # Verifica si fue revocado
    )
    return decoded_token

4.3. Claims Disponibles en Token Decodificado

decoded_token = auth.verify_id_token(id_token)

# Claims estandar
uid = decoded_token['uid']              # ID unico del usuario
email = decoded_token.get('email')      # Email (si existe)
email_verified = decoded_token.get('email_verified', False)
phone = decoded_token.get('phone_number')

# Claims de Firebase
auth_time = decoded_token['auth_time']  # Timestamp de autenticacion
iat = decoded_token['iat']              # Issued at
exp = decoded_token['exp']              # Expiration
iss = decoded_token['iss']              # Issuer
aud = decoded_token['aud']              # Audience (project ID)

# Claims de sign-in
firebase_claims = decoded_token.get('firebase', {})
sign_in_provider = firebase_claims.get('sign_in_provider')
# Valores: 'password', 'google.com', 'apple.com', 'phone', etc.

# Custom claims (definidos por MedTime)
tier = decoded_token.get('medtime_tier')
role = decoded_token.get('medtime_role')

4.4. Validaciones que Realiza verify_id_token()

Validaciones Automaticas:
  header:
    alg: "Debe ser RS256"
    kid: "Debe corresponder a clave publica de Firebase"

  payload:
    exp: "Debe ser futuro"
    iat: "Debe ser pasado"
    aud: "Debe coincidir con project_id"
    iss: "Debe ser https://securetoken.google.com/{project_id}"
    sub: "Debe ser string no vacio (uid)"
    auth_time: "Debe ser pasado"

  signature:
    verificacion: "Con clave publica de Google"
    keys_url: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"

5. Custom Claims

5.1. Asignar Custom Claims

from firebase_admin import auth

def set_user_claims(uid: str, claims: dict) -> None:
    """Asigna custom claims a un usuario.

    Args:
        uid: ID del usuario
        claims: Dict con claims (max 1000 bytes)

    Example:
        set_user_claims("uid123", {
            "medtime_tier": "pro",
            "medtime_role": "patient",
            "medtime_permissions": ["read_own", "write_own"]
        })
    """
    auth.set_custom_user_claims(uid, claims)

5.2. Leer Custom Claims

def get_user_claims(uid: str) -> dict:
    """Obtiene los custom claims de un usuario."""
    user = auth.get_user(uid)
    return user.custom_claims or {}

5.3. Claims Especificos de MedTime

from dataclasses import dataclass
from enum import Enum
from typing import List
import json

class MedTimeTier(str, Enum):
    FREE = "free"
    PRO = "pro"
    PERFECT = "perfect"

class MedTimeRole(str, Enum):
    PATIENT = "patient"
    CAREGIVER = "caregiver"
    DEPENDENT = "dependent"
    ADMIN = "admin"

@dataclass
class MedTimeClaims:
    tier: MedTimeTier
    role: MedTimeRole
    device_id: str
    permissions: List[str]

    def to_dict(self) -> dict:
        return {
            "medtime_tier": self.tier.value,
            "medtime_role": self.role.value,
            "medtime_device_id": self.device_id,
            "medtime_permissions": self.permissions
        }

    def validate_size(self) -> bool:
        """Verifica que claims no excedan 1000 bytes."""
        return len(json.dumps(self.to_dict()).encode()) <= 1000


def assign_medtime_claims(
    uid: str,
    tier: MedTimeTier,
    role: MedTimeRole,
    device_id: str
) -> None:
    """Asigna claims de MedTime a un usuario."""

    permissions = get_permissions_for_tier(tier)

    claims = MedTimeClaims(
        tier=tier,
        role=role,
        device_id=device_id,
        permissions=permissions
    )

    if not claims.validate_size():
        raise ValueError("Claims exceden 1000 bytes")

    auth.set_custom_user_claims(uid, claims.to_dict())


def get_permissions_for_tier(tier: MedTimeTier) -> List[str]:
    """Retorna permisos segun tier."""
    base = ["read_own", "write_own"]

    if tier == MedTimeTier.FREE:
        return base

    if tier == MedTimeTier.PRO:
        return base + ["read_dependents", "multi_device"]

    if tier == MedTimeTier.PERFECT:
        return base + [
            "read_dependents",
            "manage_dependents",
            "multi_device",
            "advanced_analytics"
        ]

    return base

6. Gestion de Usuarios

6.1. Obtener Usuario

from firebase_admin import auth

# Por UID
user = auth.get_user(uid)

# Por email
user = auth.get_user_by_email(email)

# Por telefono
user = auth.get_user_by_phone_number(phone)

# Atributos disponibles
print(user.uid)
print(user.email)
print(user.email_verified)
print(user.phone_number)
print(user.display_name)
print(user.photo_url)
print(user.disabled)
print(user.custom_claims)
print(user.provider_data)  # Lista de providers vinculados
print(user.tokens_valid_after_time)  # Para revocacion

6.2. Listar Usuarios

def list_all_users():
    """Lista todos los usuarios (paginado)."""
    page = auth.list_users()

    while page:
        for user in page.users:
            print(f"User: {user.uid}, Email: {user.email}")

        page = page.get_next_page()

# Con limite
def list_users_batch(max_results: int = 100):
    """Lista usuarios con limite."""
    page = auth.list_users(max_results=max_results)
    return [user for user in page.users]

6.3. Crear Usuario

def create_user(
    email: str,
    password: str,
    display_name: str = None
) -> auth.UserRecord:
    """Crea un nuevo usuario."""

    user = auth.create_user(
        email=email,
        email_verified=False,
        password=password,
        display_name=display_name,
        disabled=False
    )

    return user

6.4. Actualizar Usuario

def update_user(uid: str, **kwargs) -> auth.UserRecord:
    """Actualiza propiedades de usuario.

    Kwargs soportados:
        - email: str
        - email_verified: bool
        - phone_number: str
        - password: str
        - display_name: str
        - photo_url: str
        - disabled: bool
    """
    user = auth.update_user(uid, **kwargs)
    return user

# Ejemplo: Deshabilitar usuario
auth.update_user(uid, disabled=True)

# Ejemplo: Verificar email manualmente
auth.update_user(uid, email_verified=True)

6.5. Eliminar Usuario

def delete_user(uid: str) -> None:
    """Elimina un usuario permanentemente."""
    auth.delete_user(uid)

def delete_users_batch(uids: list) -> auth.DeleteUsersResult:
    """Elimina multiples usuarios."""
    result = auth.delete_users(uids)
    print(f"Deleted: {result.success_count}")
    print(f"Failed: {result.failure_count}")
    return result

7. Revocacion de Tokens

7.1. Revocar Refresh Tokens

from firebase_admin import auth

def revoke_user_tokens(uid: str) -> None:
    """Revoca todos los refresh tokens de un usuario.

    Fuerza re-autenticacion en todos los dispositivos.
    """
    auth.revoke_refresh_tokens(uid)

# Casos de uso:
# - Cambio de password
# - Compromiso de cuenta detectado
# - Logout de todos los dispositivos
# - Cambio de tier/permisos

7.2. Verificar Tiempo de Revocacion

def check_token_revocation(id_token: str) -> bool:
    """Verifica si un token fue emitido antes de revocacion."""
    try:
        decoded = auth.verify_id_token(id_token, check_revoked=True)
        return True
    except auth.RevokedIdTokenError:
        return False

8. Custom Tokens

8.1. Crear Custom Token

def create_custom_token(uid: str, additional_claims: dict = None) -> bytes:
    """Crea un custom token para autenticacion.

    Uso: Integrar sistemas de autenticacion externos.

    Args:
        uid: ID unico del usuario (puede ser de sistema externo)
        additional_claims: Claims adicionales (max 1000 bytes)

    Returns:
        bytes: Token JWT que el cliente usa para signInWithCustomToken()
    """
    custom_token = auth.create_custom_token(uid, additional_claims)
    return custom_token

8.2. Flujo de Custom Token

Flujo Custom Token:
  1_backend:
    accion: "Valida credenciales externas"
    resultado: "Obtiene UID del usuario"

  2_create:
    accion: "auth.create_custom_token(uid, claims)"
    resultado: "Token JWT firmado por service account"

  3_send:
    accion: "Envia custom token al cliente"
    transporte: "HTTPS"

  4_client:
    accion: "signInWithCustomToken(customToken)"
    firebase: "Intercambia por ID token real"

  5_result:
    accion: "Cliente obtiene ID token"
    uso: "Autenticar requests al backend"

9. Manejo de Errores

9.1. Excepciones Comunes

from firebase_admin import auth
from firebase_admin.exceptions import FirebaseError

def handle_auth_errors(id_token: str):
    """Ejemplo de manejo de errores."""
    try:
        decoded = auth.verify_id_token(id_token)
        return decoded

    except auth.InvalidIdTokenError as e:
        # Token malformado o firma invalida
        raise AuthenticationError(
            code="INVALID_TOKEN",
            message="Token de autenticacion invalido"
        )

    except auth.ExpiredIdTokenError as e:
        # Token expirado (> 1 hora)
        raise AuthenticationError(
            code="EXPIRED_TOKEN",
            message="Token expirado, por favor re-autentique"
        )

    except auth.RevokedIdTokenError as e:
        # Token revocado (check_revoked=True)
        raise AuthenticationError(
            code="REVOKED_TOKEN",
            message="Sesion revocada, por favor inicie sesion nuevamente"
        )

    except auth.CertificateFetchError as e:
        # No se pudieron obtener claves publicas
        raise AuthenticationError(
            code="SERVICE_ERROR",
            message="Error de servicio de autenticacion"
        )

    except auth.UserDisabledError as e:
        # Usuario deshabilitado
        raise AuthenticationError(
            code="USER_DISABLED",
            message="Cuenta deshabilitada"
        )

    except FirebaseError as e:
        # Otros errores de Firebase
        raise AuthenticationError(
            code="FIREBASE_ERROR",
            message=str(e)
        )

9.2. Codigos de Error Comunes

Error Codigo Causa Accion
InvalidIdTokenError INVALID_ARGUMENT Token malformado Solicitar nuevo login
ExpiredIdTokenError EXPIRED Token > 1 hora Refresh automatico
RevokedIdTokenError REVOKED Token revocado Forzar re-login
UserNotFoundError NOT_FOUND UID no existe Crear cuenta o error
EmailAlreadyExistsError ALREADY_EXISTS Email duplicado Login existente

10. Integracion con MedTime Backend

10.1. Middleware de Autenticacion

from functools import wraps
from firebase_admin import auth
from flask import request, g

def require_auth(f):
    """Decorator para requerir autenticacion."""
    @wraps(f)
    def decorated(*args, **kwargs):
        # Obtener token del header
        auth_header = request.headers.get('Authorization', '')
        if not auth_header.startswith('Bearer '):
            return {"error": "Missing authorization"}, 401

        id_token = auth_header.split('Bearer ')[1]

        try:
            # Verificar token
            decoded = auth.verify_id_token(id_token)
            g.user = decoded
            g.uid = decoded['uid']
            g.tier = decoded.get('medtime_tier', 'free')
            g.role = decoded.get('medtime_role', 'patient')

        except Exception as e:
            return {"error": str(e)}, 401

        return f(*args, **kwargs)

    return decorated

@app.route('/api/v1/medications')
@require_auth
def get_medications():
    # g.uid, g.tier, g.role disponibles
    pass

10.2. Configuracion de RLS Context

async def set_rls_context_from_token(
    db_conn,
    decoded_token: dict
) -> None:
    """Configura contexto RLS basado en token verificado."""

    uid = decoded_token['uid']
    tier = decoded_token.get('medtime_tier', 'free')
    role = decoded_token.get('medtime_role', 'patient')

    # Configurar variables de sesion para RLS
    await db_conn.execute(
        "SET LOCAL app.current_user_id = $1",
        uid
    )
    await db_conn.execute(
        "SET LOCAL app.user_tier = $1",
        tier
    )
    await db_conn.execute(
        "SET LOCAL app.user_role = $1",
        role
    )

    # Configurar permisos segun tier
    max_devices = {"free": 1, "pro": 3, "perfect": 5}.get(tier, 1)
    await db_conn.execute(
        "SET LOCAL app.max_devices = $1",
        max_devices
    )

11. Mejores Practicas

11.1. Seguridad

Mejores Practicas Seguridad:

  service_account:
    - Nunca commitear a repositorio
    - Almacenar en Secret Manager
    - Rotar cada 90 dias
    - Usar service account dedicado por ambiente

  verificacion_tokens:
    - Siempre verificar en servidor
    - Nunca confiar en claims del cliente
    - Usar check_revoked para operaciones sensibles

  custom_claims:
    - No almacenar datos sensibles
    - Mantener claims pequenos (< 1000 bytes)
    - Validar claims en backend

11.2. Performance

Mejores Practicas Performance:

  caching:
    - Cachear claves publicas (respeta max-age)
    - verify_id_token ya cachea internamente

  verificacion:
    - check_revoked agrega latencia
    - Usar solo cuando necesario

  inicializacion:
    - Inicializar Admin SDK una vez al startup
    - Reusar instancia globalmente

11.3. Ejemplo Completo

# config/firebase.py
import firebase_admin
from firebase_admin import credentials
import os

def init_firebase():
    """Inicializa Firebase Admin SDK."""
    if firebase_admin._apps:
        return  # Ya inicializado

    if os.getenv('FIREBASE_CREDENTIALS'):
        # Produccion: desde Secret Manager
        import json
        cred_dict = json.loads(os.getenv('FIREBASE_CREDENTIALS'))
        cred = credentials.Certificate(cred_dict)
    else:
        # Desarrollo: desde archivo
        cred = credentials.Certificate('service-account.json')

    firebase_admin.initialize_app(cred)

# Llamar al iniciar la aplicacion
init_firebase()

12. Referencias

12.1. Documentacion Oficial

Recurso URL
Admin SDK Setup https://firebase.google.com/docs/admin/setup
Verify ID Tokens https://firebase.google.com/docs/auth/admin/verify-id-tokens
Custom Claims https://firebase.google.com/docs/auth/admin/custom-claims
Manage Users https://firebase.google.com/docs/auth/admin/manage-users
Python SDK Reference https://firebase.google.com/docs/reference/admin/python

12.2. Documentos Internos Relacionados

ID Documento Relacion
INV-015 Firebase Authentication Contexto general
TECH-SEC-SRV-001 05-seguridad-servidor.md Implementacion
MTS-AUTH-001 Autenticacion Requisitos funcionales

Documento generado por SpecQueen + SecurityDrone Fuentes: Firebase Documentation, Google Cloud Identity Platform