🔥

Firebase Authentication — NutriFlow SaaS

Guía de implementación completa · Web + Firestore Security Rules

Firebase Auth React + Vite Firestore JWT · OAuth2
🥗

NutriFlow SaaS

Plataforma B2B para dietistas y nutricionistas · Firebase Project: nutriflow-prod

3
Proveedores
2
Roles
JWT
Tokens
Flujo de autenticación — NutriFlow
Usuario
Dietista / Paciente
AuthPanel
Componente React
Firebase Auth
SDK · Google · JWT
↓ onAuthStateChanged(user)
ID Token (JWT)
1h · identidad
Firestore
Security Rules
1
Provisionamiento — firebase.json
Habilitar proveedores via CLI y desplegar configuración
⚠️
CRÍTICO — deploying auth config Tras modificar firebase.json, es obligatorio ejecutar npx firebase-tools deploy --only auth para que los cambios tengan efecto en el backend. Sin este paso, los proveedores OAuth no generan sus clientes automáticamente.
firebase.json JSON
{
  "auth": {
    "providers": {
      "anonymous": true,
      "emailPassword": true,
      "googleSignIn": {
        "oAuthBrandDisplayName": "NutriFlow",
        "supportEmail": "soporte@nutriflow.app",
        "authorizedRedirectUris": [
          "https://nutriflow.app",
          "http://localhost"
        ]
      }
    },
    "authorizedDomains": [
      "nutriflow.app",
      "localhost"
    ]
  },
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{ "source": "**", "destination": "/index.html" }]
  }
}
💡
Authorized Domains — regla crítica Añadir solo el dominio, sin protocolo ni puerto. Usar localhost, NO http://localhost:5173. De lo contrario obtendrás [firebase_auth/unauthorized-domain] en desarrollo.
terminal BASH
# Instalar Firebase CLI e inicializar proyecto
npx -y firebase-tools@latest login
npx -y firebase-tools@latest projects:create nutriflow-prod

# Desplegar configuración de auth (OBLIGATORIO después de editar firebase.json)
npx -y firebase-tools@latest deploy --only auth

# Verificar proveedores activos
npx -y firebase-tools@latest auth:export users.json --project nutriflow-prod
2
Módulo Firebase Auth SDK
src/firebase/auth.js — inicialización y funciones de auth
src/firebase/config.js JS
import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: "AIzaSy...nutriflow-key",
  authDomain: "nutriflow-prod.firebaseapp.com",
  projectId: "nutriflow-prod",
  storageBucket: "nutriflow-prod.appspot.com",
  messagingSenderId: "482019374650",
  appId: "1:482019374650:web:a3f7c2"
};

export const app = initializeApp(firebaseConfig);
src/firebase/auth.js JS
import {
  getAuth, connectAuthEmulator,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signInWithPopup, GoogleAuthProvider,
  signInAnonymously,
  onAuthStateChanged,
  signOut
} from "firebase/auth";
import { app } from "./config";

const auth = getAuth(app);

// Conectar al emulador local en desarrollo
if (location.hostname === "localhost") {
  connectAuthEmulator(auth, "http://localhost:9099");
}

/** Registro de dietista con email/password */
export const registerDietista = (email, password) =>
  createUserWithEmailAndPassword(auth, email, password);

/** Login con email/password */
export const loginEmail = (email, password) =>
  signInWithEmailAndPassword(auth, email, password);

/** Google Sign-In popup — para pacientes */
export const loginGoogle = () => {
  const provider = new GoogleAuthProvider();
  provider.addScope("email");
  return signInWithPopup(auth, provider);
};

/** Acceso anónimo — demo sin registro */
export const loginAnonimo = () =>
  signInAnonymously(auth);

/** Observer de estado — usar en App.jsx */
export const onAuthChange = (callback) =>
  onAuthStateChanged(auth, callback);

/** Cerrar sesión */
export const logout = () => signOut(auth);

export { auth };
3
Componente AuthPanel.jsx
UI de login con los 3 proveedores + observer de estado
src/components/AuthPanel.jsx JSX
import { useState, useEffect } from "react";
import {
  registerDietista, loginEmail,
  loginGoogle, loginAnonimo,
  onAuthChange, logout
} from "../firebase/auth";

export default function AuthPanel() {
  const [user, setUser] = useState(null);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState(null);

  // Observar cambios de sesión (se limpia al desmontar)
  useEffect(() => {
    const unsubscribe = onAuthChange((u) => setUser(u));
    return unsubscribe;
  }, []);

  const handleError = (err) =>
    setError(err.code === "auth/unauthorized-domain"
      ? "Dominio no autorizado. Añade localhost a Authorized Domains."
      : err.message);

  if (user) {
    return (
      <div className="dashboard">
        <p>Bienvenido, <strong>{user.displayName || user.email}</strong></p>
        <p>UID: {user.uid}</p>
        <p>Email verificado: {user.emailVerified ? "✓" : "✗"}</p>
        <button onClick={logout}>Cerrar sesión</button>
      </div>
    );
  }

  return (
    <div className="auth-panel">
      <h2>Acceder a NutriFlow</h2>

      {/* Email/Password — dietistas */}
      <input type="email" value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="email@nutriflow.app" />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button onClick={() =>
        loginEmail(email, password).catch(handleError)}>
        Entrar con email
      </button>
      <button onClick={() =>
        registerDietista(email, password).catch(handleError)}>
        Registrarme como dietista
      </button>

      {/* Google — pacientes */}
      <button onClick={() =>
        loginGoogle().catch(handleError)}>
        Entrar con Google (pacientes)
      </button>

      {/* Anónimo — demo */}
      <button onClick={() =>
        loginAnonimo().catch(handleError)}>
        Ver demo sin registro
      </button>

      {error && <p className="error">{error}</p>}
    </div>
  );
}
onAuthStateChanged es el patrón correcto Usar siempre el observer en lugar de leer auth.currentUser directamente. El observer se dispara en cada cambio de sesión y gestiona automáticamente la persistencia de tokens en localStorage.
4
Firestore Security Rules
Protección de datos por rol y propiedad con request.auth
firestore.rules RULES
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // ── Perfiles de dietistas ─────────────────────────────
    match /dietistas/{userId} {
      // Cada dietista solo lee y escribe su propio perfil
      allow read, write: if request.auth != null
        && request.auth.uid == userId;
    }

    // ── Pacientes: solo el dietista propietario ───────────
    match /patients/{patientId} {
      allow read, write: if request.auth != null
        && request.auth.uid == resource.data.owner_uid;

      // Crear paciente nuevo: el campo owner_uid debe coincidir
      allow create: if request.auth != null
        && request.resource.data.owner_uid == request.auth.uid;
    }

    // ── Planes de comidas: requiere email verificado ──────
    match /meal_plans/{planId} {
      allow read: if request.auth != null
        && request.auth.uid == resource.data.owner_uid;

      // Solo dietistas con email verificado pueden crear planes
      allow create: if request.auth != null
        && request.auth.token.email_verified == true
        && request.resource.data.owner_uid == request.auth.uid;

      allow update, delete: if request.auth != null
        && request.auth.uid == resource.data.owner_uid;
    }

    // ── Perfil del paciente: solo él mismo puede leer ─────
    match /patient_profiles/{uid} {
      allow read: if request.auth != null
        && request.auth.uid == uid;
      allow write: if request.auth != null
        && request.auth.uid == uid;
    }

    // ── Bloquear todo lo demás por defecto ────────────────
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
Colección Regla clave Descripción
/dietistas/{userId} uid == userId Cada dietista gestiona solo su perfil
/patients/{patientId} uid == resource.data.owner_uid Paciente pertenece al dietista propietario
/meal_plans/{planId} email_verified == true Solo dietistas verificados pueden crear planes
/patient_profiles/{uid} uid == auth.uid Cada paciente lee/escribe solo su perfil
5
Gestión de Tokens JWT
ID Token · Refresh Token · Claims de usuario
Tipos de token en Firebase Auth
ID Token
eyJhbGciOiJSUzI1NiIsImtpZCI6IjA2NjMxM... TTL: 1h
Refresh Token
AMf-vBx7kJHs3mP9nQf2yWz8A1RoT4vCi... TTL: largo · renovable
🔑 Claims estándar del JWT

request.auth.uid — UID único global
request.auth.token.email — email del usuario
request.auth.token.email_verified — verificación
request.auth.token.name — nombre display

♻️ Renovación automática

El SDK de Firebase renueva el ID Token automáticamente antes de que expire (1h). No es necesario gestionar la renovación manualmente. El Refresh Token persiste en localStorage o IndexedDB según la plataforma.

Obtener ID Token para llamadas a tu backend JS
import { auth } from "./firebase/auth";

// Obtener el token actual (fuerza renovación si expira pronto)
const getToken = async () => {
  const user = auth.currentUser;
  if (!user) throw new Error("No hay sesión activa");
  return user.getIdToken(/* forceRefresh= */ false);
};

// Enviar al backend NutriFlow API
const fetchPacientes = async () => {
  const token = await getToken();
  const res = await fetch("/api/pacientes", {
    headers: { "Authorization": `Bearer ${token}` }
  });
  return res.json();
};
Checklist de deploy
Verificar antes de subir a producción
Proveedores Email/Password, Google y Anónimo habilitados firebase.json + npx firebase-tools deploy --only auth
Authorized Domains configurados (nutriflow.app + localhost) Sin protocolo, sin puerto. Solo el dominio desnudo.
Auth module separado (src/firebase/auth.js) Con emulador condicional para desarrollo local (port 9099)
onAuthStateChanged como fuente única de verdad del usuario Suscripción limpiada en useEffect return
Firestore Security Rules con request.auth Deny-all por defecto · owner_uid · email_verified para planes
Manejo de error auth/unauthorized-domain Mensaje amigable al usuario si el dominio no está autorizado
Verificación de email obligatoria para dietistas sendEmailVerification(user) tras createUserWithEmailAndPassword
Custom Claims para rol pro/patient via Admin SDK Cloud Function onUserCreate para asignar rol automáticamente