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
Refresh Token
🔑 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