Autenticación

Autenticación

La FE API usa un esquema simple de dos capas:

  1. Credenciales de larga duración (apiKeyId + apiSecret) — emitidas en el panel de Digimart, perduran hasta que las revoques.
  2. JWT de corta duración (1 hora) — obtenido haciendo POST /login con las credenciales del paso 1. Se usa en el header Authorization: Bearer <token> de cada llamada subsiguiente.

¿Por qué este patrón?

Tener una key estática que viaja en CADA request expone el secret en logs, tráfico de red, y caches. El JWT corto se firma en memoria y se rota cada hora, así un leak temporal se ventila solo.

El FE_API_JWT_SECRET del lado del servidor se puede rotar sin invalidar tus credenciales — los JWTs en vuelo expiran en ≤1h. Tus credenciales (apiKeyId + secret) se rotan independientemente desde el panel de Digimart.

El apiKeyId

Formato: fak_ + 12 caracteres hexadecimales (ej. fak_a1b2c3d4e5f6).

Es público — no es sensible. Se puede loguear, mostrar en UI, embeber en URLs internas. Equivale al "username" en el intercambio del /login.

El apiSecret

Formato: 64 caracteres hexadecimales (32 bytes random).

Es sensible. Trátalo como una contraseña:

  • ✅ Guárdalo en una variable de entorno o secret manager (AWS Secrets Manager, Google Secret Manager, Vault).
  • ✅ Restringe el acceso al archivo .env (chmod 600).
  • ❌ NO lo comitees a git.
  • ❌ NO lo loguees en archivos de aplicación.
  • ❌ NO lo expongas en código del frontend del navegador — esto debe vivir EXCLUSIVAMENTE en tu backend.

Flujo de /login

POST /api/v1/fe/login

Request

{
  "apiKeyId": "fak_a1b2c3d4e5f6",
  "apiSecret": "abcd1234ef56..."
}

Response (200)

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "environment": "ecf",
  "scopes": [
    "invoices:write",
    "invoices:read",
    "usage:read",
    "billing:read"
  ]
}

Response (401)

{
  "message": "Invalid credentials",
  "statusCode": 401
}
⚠️

Para evitar timing attacks, retornamos el mismo Invalid credentials en TODOS los modos de fallo (key inexistente, secret incorrecto, key revocada, key expirada). No intentes inferir cuál fue el problema del mensaje — revisa el panel de Digimart.

Usando el JWT

Una vez que tengas el accessToken, mándalo en el header Authorization de cada request:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

El servidor valida:

  1. Firma del JWT — debe estar firmado con FE_API_JWT_SECRET.
  2. Expiraciónexp no debe estar en el pasado.
  3. Scope — el JWT debe tener el scope requerido por el endpoint (ej. POST /invoices requiere invoices:write).
  4. Estado de la key — la DgiiApiKey subyacente debe seguir active. Si la revocas, los JWTs nuevos no se emitirán, y los en vuelo expiran en máximo 30 segundos (refresh interno).

Scopes

Un scope es un permiso atómico. Los scopes asignados a una key se incluyen en el JWT emitido al hacer /login:

ScopePermite
invoices:writePOST /invoices
invoices:readGET /invoices, GET /invoices/:id
usage:readGET /usage
billing:readGET /billing/current-period

Al crear una key puedes restringir sus scopes. Una key con solo invoices:read podría usarse en un dashboard de monitoreo sin riesgo de que emita nada.

Rotación de credenciales

Cuando necesites rotar (sospecha de leak, cambio de equipo, etc.):

  1. En el panel de Digimart, click "Nueva API key" → genera el nuevo par.
  2. Despliega el nuevo par a tu servidor.
  3. Verifica que el nuevo par funcione (haz una llamada de prueba).
  4. Click en el ícono 🗑️ junto a la key vieja → "Revocar".
  5. La key vieja deja de funcionar inmediatamente (cache invalidado en menos de 30 segundos).

Puedes tener N keys activas simultáneas. Crea una por entorno (producción, staging, test) o por servicio (api-server, cron-worker).

Errores comunes

ErrorCausaSolución
401 Missing bearer tokenNo mandaste el header Authorization o no usaste el prefijo Bearer Agrega Authorization: Bearer <token>
401 Invalid or expired tokenJWT mal-firmado, expirado, o decodificableVuelve a hacer POST /login
401 API key has been revokedLa key subyacente fue revocada o expiróCrea una key nueva y rota credenciales
403 Missing required scope(s): XEl JWT no tiene el scope que el endpoint requiereCrea una key con scopes que incluyan los necesarios

Lifetime del JWT

  • Default: 1 hora.
  • No es configurable desde tu lado — está fijo en el servidor.
  • No hay refresh tokens. Cuando expire, vuelve a hacer /login — toma ~200ms, no es costoso.

Patrón recomendado para servicios backend:

let cachedToken = null;
let cachedExpiry = 0;
 
async function getToken() {
  if (cachedToken && Date.now() < cachedExpiry - 60_000) {
    return cachedToken;
  }
  const resp = await fetch('/api/v1/fe/login', { /* ... */ });
  const { accessToken, expiresIn } = await resp.json();
  cachedToken = accessToken;
  cachedExpiry = Date.now() + expiresIn * 1000;
  return cachedToken;
}

El - 60_000 te da un margen de 1 minuto contra clock skew.