@boolean-packages/boolean-auth-sdk
v0.1.0
Published
Isomorphic SDK to consume cidi-auth and auth.back from Node (NestJS) and browser (React)
Readme
@boolean/auth-sdk (TypeScript) — Isomórfico para frontend y backend
SDK común para conectar aplicaciones frontend (React/Vite) y backend (NestJS/Express) con los proxies de autenticación cidi-auth y auth.back, que firman JWT a través de tokens.back.
Browser / Node ──▶ cidi-auth / auth.back ──▶ tokens.back (.well-known/jwks.json)@boolean/auth-sdk: cliente HTTP + verificador de JWT conjose.@boolean/auth-sdk/react:<AuthProvider>+useAuth()para gestionar sesión en React.@boolean/auth-sdk/nestjs:AuthModule,AuthGuard,@CurrentUser()para NestJS.
Instalación
npm install @boolean/auth-sdk jose
# Integraciones opcionales
npm install react # sólo si usás @boolean/auth-sdk/react
npm install @nestjs/common @nestjs/core rxjs # sólo si usás @boolean/auth-sdk/nestjs
npm install reflect-metadata # NestJS decoradoresBuild, tests y typecheck
cd sdks/typescript
npm install
npm run typecheck
npm test
npm run build # genera ESM + CJS + .d.ts en dist/Frontend React (Vite)
appIdes opcional. Sólo es obligatorio para el flow deloginWithCidi(cidi-auth necesitaapp_iden el payload). Si tu app habla conauth.back(login conusuario/contraseña, refresh,me,findByCuil, etc.) no necesitás declararlo.
A. App que se autentica vía CIDI (ejemplo juzgado.front)
Variables de entorno (
.env):VITE_CIDI_API_URL=https://cidi-auth-api.boolean.com.ar/v1 VITE_CIDI_APP_ID=42 VITE_CIDI_LOGIN_URL=https://cidi-login.boolean.com.arConfigurar el cliente y el provider (
src/main.tsx):import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { AuthClient, LocalStorageTokenStorage } from "@boolean/auth-sdk"; import { AuthProvider } from "@boolean/auth-sdk/react"; const authClient = new AuthClient({ baseUrl: import.meta.env.VITE_CIDI_API_URL, appId: Number(import.meta.env.VITE_CIDI_APP_ID), // requerido para loginWithCidi }); ReactDOM.createRoot(document.getElementById("root")!).render( <AuthProvider client={authClient} storage={new LocalStorageTokenStorage()}> <App /> </AuthProvider> );Callback de CIDI (
src/pages/CidiCallback.tsx):import { useEffect } from "react"; import { useAuth } from "@boolean/auth-sdk/react"; import { useNavigate } from "react-router-dom"; export function CidiCallback() { const navigate = useNavigate(); const { loginWithCidi, isLoading, user } = useAuth(); useEffect(() => { const params = new URLSearchParams(window.location.search); const cookie = params.get("hashcookie"); if (cookie) { loginWithCidi(cookie).then(() => navigate("/")); } }, [loginWithCidi, navigate]); if (isLoading) return <p>Iniciando sesión…</p>; if (user) return <p>Sesión iniciada como {user.email}</p>; return <p>No se recibió cookie de CIDI</p>; }Login local (desarrollo):
const { loginWithCredentials } = useAuth(); await loginWithCredentials(username, password);Hook
useAuth()exponeuser,isAuthenticated,isLoading,loginWithCidi,loginWithCredentials,refresh,logout,refreshUser,getAccessToken.Interceptor Axios con auto-refresh:
import axios from "axios"; import { AuthClient, MemoryTokenStorage } from "@boolean/auth-sdk"; const api = axios.create({ baseURL: "/api" }); const authClient = new AuthClient({ baseUrl: import.meta.env.VITE_CIDI_API_URL }); const storage = new MemoryTokenStorage(); api.interceptors.request.use(async (config) => { const token = await storage.getAccessToken(); if (token) config.headers.Authorization = `Token ${token}`; return config; }); api.interceptors.response.use( (r) => r, async (error) => { if (error.response?.status === 401) { const refreshToken = await storage.getRefreshToken(); if (refreshToken) { const tokens = await authClient.refresh(refreshToken); await storage.setTokens({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken }); error.config.headers.Authorization = `Token ${tokens.accessToken}`; return api.request(error.config); } } return Promise.reject(error); } );Ruta protegida:
function ProtectedRoute({ children }: { children: JSX.Element }) { const { isAuthenticated, isLoading } = useAuth(); if (isLoading) return <p>Verificando sesión…</p>; if (!isAuthenticated) return <Navigate to="/login" replace />; return children; }
B. App que se autentica sólo contra auth.back (sin CIDI)
Si tu frontend usa únicamente los endpoints de auth.back (login con usuario/contraseña, refresh,
perfil), no pases appId:
Variables de entorno (
.env):VITE_AUTH_API_URL=https://auth-api.boolean.com.arConfigurar el cliente y el provider (
src/main.tsx):import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import { AuthClient, LocalStorageTokenStorage } from "@boolean/auth-sdk"; import { AuthProvider } from "@boolean/auth-sdk/react"; // Sin appId: loginWithCredentials, refresh, me, logout, findByCuil funcionan igual. const authClient = new AuthClient({ baseUrl: import.meta.env.VITE_AUTH_API_URL, }); ReactDOM.createRoot(document.getElementById("root")!).render( <AuthProvider client={authClient} storage={new LocalStorageTokenStorage()}> <App /> </AuthProvider> );Pantalla de login (
src/pages/Login.tsx):import { useAuth } from "@boolean/auth-sdk/react"; export function Login() { const { loginWithCredentials, isLoading, user } = useAuth(); async function onSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const form = new FormData(e.currentTarget); await loginWithCredentials( String(form.get("username")), String(form.get("password")), ); } if (user) return <p>Sesión activa: {user.email}</p>; return ( <form onSubmit={onSubmit}> <input name="username" placeholder="Usuario" /> <input name="password" type="password" placeholder="Contraseña" /> <button disabled={isLoading}>Ingresar</button> </form> ); }Si más adelante esta misma app necesita también
loginWithCidi, podés pasar elappIden la llamada puntual:loginWithCidi(cookie, 42). No hace falta reconfigurar el cliente.
Backend NestJS
Módulo global:
import { Module } from "@nestjs/common"; import { APP_GUARD, Reflector } from "@nestjs/core"; import { AuthModule, AuthGuard, AUTH_SDK_VERIFIER, AuthModuleOptions, IS_PUBLIC_KEY, } from "@boolean/auth-sdk/nestjs"; class PublicAwareGuard extends AuthGuard { constructor(reflector: Reflector, ...args: ConstructorParameters<typeof AuthGuard>) { super(...args); this.reflector = reflector; } private reflector: Reflector; async canActivate(ctx: any) { const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ ctx.getHandler(), ctx.getClass(), ]); if (isPublic) return true; return super.canActivate(ctx); } } @Module({ imports: [ AuthModule.forRoot({ jwksUrl: process.env.JWKS_URL!, algorithms: ["RS256"], guard: { headerPrefixes: ["token", "bearer"], requiredTokenType: "access" }, } satisfies AuthModuleOptions), ], providers: [ { provide: APP_GUARD, useClass: PublicAwareGuard }, ], }) export class AppModule {}Controller + decorator:
import { Controller, Get } from "@nestjs/common"; import { CurrentUser, Public, AuthRequestUser } from "@boolean/auth-sdk/nestjs"; @Controller("cases") export class CasesController { @Get() list(@CurrentUser() user: AuthRequestUser) { return { userId: user.userId, roles: user.roles }; } @Public() @Get("public") publicEndpoint() { return { ok: true }; } }Consumir cidi-auth desde NestJS:
import { Injectable } from "@nestjs/common"; import { AuthClient } from "@boolean/auth-sdk"; @Injectable() export class AuthService { private readonly client = new AuthClient({ baseUrl: process.env.CIDI_AUTH_BASE_URL!, appId: Number(process.env.CIDI_APP_ID) }); findByCuil(cuil: string, token: string) { return this.client.findByCuil(cuil, token); } }
Backend Express / Fastify
import express from "express";
import { JWTVerifier } from "@boolean/auth-sdk";
const verifier = new JWTVerifier({ jwksUrl: process.env.JWKS_URL! });
const app = express();
app.use(async (req, res, next) => {
const header = req.headers.authorization;
if (!header) return res.status(401).send("Missing token");
const [scheme, token] = header.split(" ");
if (!token || !/^token|bearer$/i.test(scheme)) return res.status(401).send("Invalid header");
try {
req.user = await verifier.verify(token, { expectedTokenType: "access" });
next();
} catch (err) {
return res.status(401).send((err as Error).message);
}
});Uso básico (browser/Node sin frameworks)
import { AuthClient, JWTVerifier, MemoryTokenStorage } from "@boolean/auth-sdk";
const client = new AuthClient({ baseUrl: "https://auth.example.com/v1" });
const storage = new MemoryTokenStorage();
const login = async () => {
const tokens = await client.loginWithCredentials("alice", "secret");
await storage.setTokens({ accessToken: tokens.accessToken, refreshToken: tokens.refreshToken });
};
const verifier = new JWTVerifier({ jwksUrl: "https://tokens.example.com/.well-known/jwks.json" });
const payload = await verifier.verify(await storage.getAccessToken()!);
console.log(payload.user_id);Manejo de errores
Todas las excepciones extienden AuthSDKError:
| Error | Contexto |
|--------------------------|----------------------------------------------|
| AuthenticationError | 401/403 del servidor de auth |
| HTTPError | Otra respuesta HTTP no exitosa |
| InvalidTokenError | Firma/claims inválidos |
| TokenExpiredError | JWT expirado (subclase de InvalidTokenError)|
Capturalas según la capa (frontend → mostrar login; backend → Unauthorized).
Opciones principales
AuthClient:
baseUrl(string) — obligatorio.appId(number | string, opcional) — sólo necesario si vas a usarloginWithCidi. Se toma como default cuando no lo pasás explícitamente. Para apps que sólo hablan conauth.backno lo declares.timeoutMs(number) — default 10 000 ms.tokenHeaderScheme— "Token" por defecto (usar "Bearer" si el backend lo exige).fetch— inyectá un fetch custom (ej.node-fetch).defaultHeaders— headers adicionales.
JWTVerifier:
jwksUrl— URL al JWKS detokens.back.localSecret— PEM (SPKI público o PKCS#8 privado) como fallback.algorithms,audience,issuer,jwksHeaders— validaciones adicionales.
Exports del paquete
@boolean/auth-sdk → client, verifier, token storage, errores, tipos
@boolean/auth-sdk/react → AuthProvider, useAuth
@boolean/auth-sdk/nestjs → AuthModule, AuthGuard, decorators, tiposNotas de despliegue
- El verificador usa
fetch; Node 18+ lo trae nativo. En Node <18, pasá un fetch custom. - El JWKS se cachea siguiendo
jose(createRemoteJWKSet). Headers se setean con User-Agent tipo Chrome para evitar bloqueos. - Para SSR/Next.js, podés usar
MemoryTokenStorageen el servidor y reenviar tokens al cliente via cookies.
