Skip to main content
Glama

Backbone.js Documentation MCP Server

by elegroag
Backbone-cap-10.md12.3 kB
# Capítulo 10. Autenticación (ESNext + ESM + Vite) Este capítulo moderniza la autenticación en aplicaciones Backbone con un enfoque sin estado y ejemplos listos para Vite (bundler) y Express (API), usando módulos ES (ESM) y JavaScript moderno (ESNext). - Objetivo: entender y aplicar Basic Auth y OAuth2 en un entorno SPA + API stateless. - Stack: Vite para frontend, Express ESM para API, `fetch` nativo en el navegador. - Requisitos: Node >= 18, proyecto con `"type": "module"` y Vite. --- ## APIs sin estado y dónde guardar la sesión - En REST, el servidor no guarda estado de tu sesión. Cada request debe incluir credenciales/tokens. - El cliente (navegador) debe persistir datos mínimos: token de acceso y tipo de token. - Opciones de almacenamiento: `sessionStorage` (caduca al cerrar la pestaña) o `localStorage` (persistente). Para este capítulo usaremos `sessionStorage`. --- ## Autenticación HTTP Basic Basic Auth envía en cada petición un encabezado `Authorization: Basic <base64(user:pass)>`. - Sólo debe usarse sobre HTTPS. - Útil para demos o entornos controlados; no para producción con usuarios reales. ### Servidor Express (ESM) – middleware de Basic Auth ```js // server/basicAuthMiddleware.mjs (ESM) export function basicAuthRequired(req, res, next) { const header = req.headers.authorization || ''; // Espera: "Basic base64(user:pass)" const [type, payload] = header.split(' '); if (type !== 'Basic' || !payload) return res.sendStatus(401); try { const [user, pass] = Buffer.from(payload, 'base64').toString('utf8').split(':'); // Demo: credenciales fijas if (user === 'john' && pass === 'doe') return next(); return res.sendStatus(401); } catch { return res.sendStatus(401); } } ``` Prerequisito: asegúrate de tener inicializada una `Region` o contenedor principal antes de enrutar. Por ejemplo, en tu arranque: ```js // En startApp() o similar // App.mainRegion = new Region({ el: '#main' }); ``` Rutas protegidas: ```js // server/routes.mjs (ESM) import express from 'express'; import { basicAuthRequired } from './basicAuthMiddleware.mjs'; import * as controller from './controller.mjs'; export const router = express.Router(); router.post('/api/contacts', basicAuthRequired, controller.createContact); router.get('/api/contacts', basicAuthRequired, controller.showContacts); router.get('/api/contacts/:contactId', basicAuthRequired, controller.findContactById); router.put('/api/contacts/:contactId', basicAuthRequired, controller.updateContact); router.delete('/api/contacts/:contactId', basicAuthRequired, controller.deleteContact); router.post('/api/contacts/:contactId/avatar', basicAuthRequired, controller.uploadAvatar); ``` Si usas `WWW-Authenticate`, el navegador puede mostrar un diálogo nativo. Para un flujo personalizado (SPA), evita ese header y muestra tu propio formulario. #### 2.2 Cliente Backbone (ESM) – vista de Login con fetch Creamos una vista de login y un servicio de autenticación reutilizable. Usa `fetch` y guarda el token en `sessionStorage`. Servicio de autenticación: ```js // src/ui/services/Auth.js (ESM) const KEY = 'auth'; export function save(type, token) { sessionStorage.setItem(KEY, `${type}:${token}`); } export function load() { const raw = sessionStorage.getItem(KEY); if (!raw) return null; const [type, token] = raw.split(':'); return { type, token }; } export function drop() { sessionStorage.removeItem(KEY); } export function buildBasic(username, password) { // btoa es suficiente en el navegador para ASCII return btoa(`${username}:${password}`); } ``` Nota: `btoa` solo maneja ASCII. Para credenciales con UTF‑8 (tildes, emojis), usa esta variante: ```js // Variante UTF-8 segura export function buildBasicUtf8(username, password) { const bytes = new TextEncoder().encode(`${username}:${password}`); let binary = ''; for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary); } ``` Inyección del header en todas las sync de Backbone: ```js // src/ui/common/setupAuth.js (ESM) import Backbone from 'backbone'; import { load } from '../services/Auth.js'; export function setupGlobalAuth() { const auth = load(); // Sobrescribimos sync para añadir Authorization si existe const originalSync = Backbone.sync; Backbone.sync = function (method, model, options = {}) { const headers = { ...(options.headers || {}) }; const a = load(); // Respeta un Authorization explícito en options.headers if (!('Authorization' in headers) && a?.type && a?.token) { headers.Authorization = `${a.type} ${a.token}`; } // Conserva otras opciones como emulateJSON/xhrFields/credentials/beforeSend return originalSync.call(this, method, model, { ...options, headers }); }; } ``` Nota: al propagar `...options`, conservas `emulateJSON`, `xhrFields`, `credentials`, `beforeSend` y demás opciones de Backbone/jQuery. Evita sobrescribir `headers.Authorization` si ya se proporciona explícitamente en una llamada puntual. Vista de Login: ```js // src/ui/modules/auth/view/LoginView.js (ESM) import Backbone from 'backbone'; import { buildBasicUtf8, save } from '../../../services/Auth.js'; export default class LoginView extends Backbone.View { get events() { return { 'submit form': 'makeLogin' }; } makeLogin(e) { e.preventDefault(); const username = this.$('#username').val(); const password = this.$('#password').val(); const token = buildBasicUtf8(username, password); fetch('/api/contacts', { headers: { Authorization: `Basic ${token}` }, }) .then((res) => { if (res.status === 401) throw new Error('UNAUTHORIZED'); save('Basic', token); Backbone.history.navigate('contacts', { trigger: true }); }) .catch((err) => { const msg = err.message === 'UNAUTHORIZED' ? 'Usuario/Clave inválidos' : 'Error desconocido'; this.$('#message').text(msg); }); } } ``` Router de Login: ```js // src/ui/modules/auth/AuthRouter.js (ESM) import Backbone from 'backbone'; import LoginView from './view/LoginView.js'; export default class AuthRouter extends Backbone.Router { initialize() { this.route('login', 'showLogin'); } showLogin() { const App = window.App; // o importa tu controlador/layout principal App.mainRegion.show(new LoginView()); } } ``` Arranque de la App: cargar sesión si existe y montar router global que soporte `logout`. ```js // src/ui/App.js (ESM) import Backbone from 'backbone'; import { load, drop } from './services/Auth.js'; import { setupGlobalAuth } from './common/setupAuth.js'; export class DefaultRouter extends Backbone.Router { initialize() { this.route('', 'home'); this.route('logout', 'logout'); } home() { this.navigate('contacts', { trigger: true, replace: true }); } logout() { drop(); this.navigate('login', { trigger: true, replace: true }); } } export function startApp() { window.App = window.App || {}; // App.mainRegion = new Region({ el: '#main' }); // usa tu Region existente setupGlobalAuth(); const auth = load(); if (!auth) window.location.replace('/#login'); App.router = new DefaultRouter(); Backbone.history.start(); } ``` --- ### 3) OAuth2 (Resource Owner Password Credentials) Para una SPA propia (frontend + backend controlados), el flujo de “password” puede servir en entornos controlados. Para producción pública, considera Authorization Code con PKCE. #### 3.1 Endpoint de token (ESM) ```js // server/oauth/token.mjs (ESM) import crypto from 'node:crypto'; const DEFAULT_EXPIRATION = 3600; // 1h const validTokens = new Map(); const refreshTokens = new Map(); function generateToken(bytes = 30) { return crypto.randomBytes(bytes).toString('base64url'); } export function authorize(body) { const { grant_type, username, password } = body || {}; if (grant_type !== 'password') return { error: 'invalid_grant' }; if (!username || !password) return { error: 'invalid_request' }; // Demo: usuario fijo if (username !== 'john' || password !== 'doe') return { error: 'invalid_grant' }; const access_token = generateToken(); const refresh_token = generateToken(); const token = { access_token, token_type: 'Bearer', expires_in: DEFAULT_EXPIRATION, refresh_token, username, }; validTokens.set(access_token, token); refreshTokens.set(refresh_token, token); setTimeout(() => validTokens.delete(access_token), DEFAULT_EXPIRATION * 1000); return token; } export function requireAuthorization(req, res, next) { const header = req.headers.authorization || ''; const [type, token] = header.split(' '); if (type !== 'Bearer' || !token) return res.sendStatus(401); if (!validTokens.has(token)) return res.sendStatus(401); return next(); } ``` Rutas: ```js // server/routes.mjs (añadir OAuth) import express from 'express'; import { authorize, requireAuthorization } from './oauth/token.mjs'; import * as controller from './controller.mjs'; export const router = express.Router(); router.post('/api/oauth/token', express.urlencoded({ extended: false }), (req, res) => { const result = authorize(req.body); if (result.error) return res.status(400).json(result); return res.json(result); }); router.get('/api/contacts', requireAuthorization, controller.showContacts); // ... resto de rutas protegidas ``` #### 3.2 Cliente – Login con `fetch` a `/api/oauth/token` ```js // src/ui/modules/auth/view/LoginView.js (variante OAuth2) import Backbone from 'backbone'; import { save } from '../../../services/Auth.js'; export default class LoginView extends Backbone.View { get events() { return { 'submit form': 'makeLogin' }; } makeLogin(e) { e.preventDefault(); const username = this.$('#username').val(); const password = this.$('#password').val(); const body = new URLSearchParams(); body.set('grant_type', 'password'); body.set('username', username); body.set('password', password); fetch('/api/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, }) .then(async (res) => { if (!res.ok) throw new Error(res.status === 401 ? 'UNAUTHORIZED' : 'BAD'); const data = await res.json(); save(data.token_type, data.access_token); Backbone.history.navigate('contacts', { trigger: true }); }) .catch((err) => { const msg = err.message === 'UNAUTHORIZED' ? 'Usuario/Clave inválidos' : 'Error desconocido'; this.$('#message').text(msg); }); } } ``` --- ### 4) Integración con Vite (proxy de API y build) - Durante desarrollo, Vite puede proxyear `/api` al servidor Express: ```ts // vite.config.ts (extracto) import { defineConfig } from 'vite'; export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, }); ``` - En producción, sirve el frontend estático (carpeta de build) y expón la API desde el mismo Express o detrás de Nginx (ver Cap. 9). --- ### 5) Seguridad y buenas prácticas - Usa siempre HTTPS para evitar exponer credenciales en claro. - Evita “Implicit Grant” en SPAs modernas; usa Authorization Code con PKCE si necesitas OAuth2 con terceros. - No guardes contraseñas ni tokens largos en `localStorage` sin expiración; prefiere expiraciones cortas y refresh tokens con rotación. - Prefiere almacenar el token en memoria (alcance de módulo) y, si necesitas persistencia limitada, refleja sólo un estado mínimo en `sessionStorage`. - Aplica una CSP estricta (Content Security Policy) para reducir el riesgo de XSS; tokens en memoria disminuyen el impacto de XSS respecto a `localStorage`. - Si usas cookies, habilita `HttpOnly`, `Secure`, `SameSite=Strict` y rotación/invalidación de sesión; evita mezclar cookies y Bearer Tokens sin un propósito claro. - Implementa rotación/revocación de refresh tokens y registra intentos fallidos de autenticación. - No ejecutes Node como root y registra logs de 401/403. --- ### 6) Resumen - Vimos Basic Auth y OAuth2 en un backend Express ESM y un frontend Backbone ESM con Vite. - Centralizamos la sesión en un servicio `Auth` y añadimos el header a todas las llamadas sobrescribiendo `Backbone.sync`. - Con Vite, el flujo local es simple con `server.proxy` y, en producción, puedes usar Nginx + PM2 (ver Cap. 9).

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/elegroag/backbone-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server