Avistadocs
Guías de Integración

Autenticación

Descripción General

La API de Avista utiliza autenticación basada en OAuth 2.0 con certificados X.509 (mTLS). Este modelo de seguridad en capas garantiza que solo los clientes autorizados con certificados válidos puedan acceder a la API.

¿Por qué mTLS?

mTLS (mutual TLS) ofrece una seguridad superior en comparación con tokens simples:

  • Autenticación mutua: Tanto el cliente como el servidor se autentican entre sí
  • No repudio: Los certificados vinculados a la cuenta garantizan la trazabilidad
  • Protección contra robo de credenciales: Incluso con clientId/clientSecret filtrados, el atacante necesitaría el certificado

Requisitos Previos

Antes de comenzar, necesitará:

Obtenga su certificado de cliente a través del portal de Avista. El certificado debe estar en formato PEM y será vinculado a su cuenta.

Solicite sus credenciales (clientId y clientSecret) en el panel de administración.

Configure su entorno para enviar el certificado en el encabezado X-SSL-Client-Cert.

El certificado X.509 debe estar vinculado a su cuenta antes de usarse. Los certificados no vinculados serán rechazados aunque sean técnicamente válidos.

Endpoint de Autenticación

POST /api/auth/token

Genera un token de acceso JWT válido por 30 minutos (1800 segundos).

El certificado X.509 debe enviarse codificado en URL en el encabezado X-SSL-Client-Cert. El sistema valida la huella digital SHA256 del certificado contra los registros vinculados a la cuenta.

Solicitud

curl -X POST https://api.avista.global/api/auth/token \
  -H "Content-Type: application/json" \
  -H "X-SSL-Client-Cert: -----BEGIN%20CERTIFICATE-----%0AMIIB..." \
  -d '{
    "clientId": "account-93-550e8400",
    "clientSecret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
  }'

Respuesta (201 Created)

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 1800
}

Ejemplo Práctico: Node.js

Instalación

npm install axios

Código Completo

const axios = require('axios');
const fs = require('fs');

// Load X.509 certificate
const certificate = fs.readFileSync('./client-cert.pem', 'utf8');
const encodedCert = encodeURIComponent(certificate);

// Request configuration
const config = {
  method: 'post',
  url: 'https://api.avista.global/api/auth/token',
  headers: {
    'Content-Type': 'application/json',
    'X-SSL-Client-Cert': encodedCert
  },
  data: {
    clientId: process.env.AVISTA_CLIENT_ID,
    clientSecret: process.env.AVISTA_CLIENT_SECRET
  }
};

// Make request
async function getToken() {
  try {
    const response = await axios(config);

    console.log('Token obtained successfully!');
    console.log('Expires in:', response.data.expires_in, 'seconds');

    return response.data.access_token;
  } catch (error) {
    console.error('Error obtaining token:', error.response?.data || error.message);
    throw error;
  }
}

getToken();

Ejemplo Práctico: Python

Instalación

pip install requests

Código Completo

import os
import requests
import urllib.parse

# Load and encode certificate
with open('client-cert.pem', 'r') as f:
    certificate = f.read()
    encoded_cert = urllib.parse.quote(certificate)

# Request configuration
url = 'https://api.avista.global/api/auth/token'
headers = {
    'Content-Type': 'application/json',
    'X-SSL-Client-Cert': encoded_cert
}
payload = {
    'clientId': os.environ.get('AVISTA_CLIENT_ID'),
    'clientSecret': os.environ.get('AVISTA_CLIENT_SECRET')
}

# Make request
try:
    response = requests.post(url, json=payload, headers=headers)
    response.raise_for_status()

    data = response.json()

    print('Token obtained successfully!')
    print(f"Expires in: {data['expires_in']} seconds")

except requests.exceptions.RequestException as e:
    print(f'Error obtaining token: {e}')
    if hasattr(e.response, 'text'):
        print(f'Response: {e.response.text}')

Uso del Token

Después de obtener el token, inclúyalo en el encabezado Authorization de todas las solicitudes:

curl -X GET https://api.avista.global/api/balance \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Renovación del Token

Los tokens expiran después de 30 minutos. Implemente lógica de renovación automática en su aplicación para evitar interrupciones.

Estrategia Recomendada

class TokenManager {
  constructor(clientId, clientSecret, certificatePath) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.certificatePath = certificatePath;
    this.token = null;
    this.expiresAt = null;
  }

  async getValidToken() {
    // Check if the token is still valid (with a 30-second margin)
    if (this.token && this.expiresAt && Date.now() < this.expiresAt - 30000) {
      return this.token;
    }

    // Renew the token
    return await this.refreshToken();
  }

  async refreshToken() {
    const response = await this.requestNewToken();
    this.token = response.access_token;
    this.expiresAt = Date.now() + (response.expires_in * 1000);
    return this.token;
  }

  async requestNewToken() {
    const fs = require('fs');
    const axios = require('axios');

    const certificate = fs.readFileSync(this.certificatePath, 'utf8');
    const encodedCert = encodeURIComponent(certificate);

    const response = await axios.post('https://api.avista.global/api/auth/token', {
      clientId: this.clientId,
      clientSecret: this.clientSecret
    }, {
      headers: {
        'Content-Type': 'application/json',
        'X-SSL-Client-Cert': encodedCert
      }
    });

    return response.data;
  }
}

// Usage
const tokenManager = new TokenManager(
  process.env.AVISTA_CLIENT_ID,
  process.env.AVISTA_CLIENT_SECRET,
  './client-cert.pem'
);

// In any request
const token = await tokenManager.getValidToken();

Validación del Certificado

El sistema realiza las siguientes validaciones sobre el certificado:

  1. Formato PEM válido: El certificado debe estar en formato PEM y codificado en URL
  2. Vinculación a la cuenta: La huella digital SHA256 del certificado debe estar registrada y vinculada a su cuenta
  3. Coincidencia de credenciales: El certificado debe pertenecer a la misma cuenta que las credenciales OAuth

Los certificados que no estén vinculados o estén vinculados a otra cuenta serán rechazados, aunque sean técnicamente válidos.

Respuestas de error

Todos los errores devueltos por POST /api/auth/token siguen la misma estructura:

{
  "statusCode": 400,
  "timestamp": "2026-04-24T16:59:25.577Z",
  "path": "/api/auth/token",
  "method": "POST",
  "code": "PUB_CERT_MALFORMED_PEM",
  "message": "Certificate could not be parsed",
  "userMessage": "The provided certificate is malformed.",
  "details": {
    "hint": "The PEM could not be parsed. Common cause: '+' characters in the base64 body must be URL-encoded as '%2B', not '%20'."
  },
  "errorId": "81421e7a5afbd9cf25212f51dac08da1"
}
  • code: identificador estable legible por máquina — use este campo para las bifurcaciones de lógica.
  • message: descripción técnica breve (en inglés, orientada a logs).
  • userMessage: descripción orientada al usuario final (en inglés).
  • details.hint: orientación práctica para los desarrolladores que integran la API.
  • errorId: identificador único de la solicitud — cítelo al contactar al soporte.

Referencia de códigos de error

PUB_CERT_HEADER_MISSING (400)

El encabezado X-SSL-Client-Cert no estaba presente en la solicitud.

Cómo resolver: incluya el encabezado con un certificado PEM codificado en URL. Si se encuentra detrás de un gateway que termina TLS (NGINX/ALB), confirme que esté configurado para reenviar $ssl_client_escaped_cert como X-SSL-Client-Cert.

PUB_CERT_MALFORMED_PEM (400)

El certificado no pudo ser analizado en el gateway o en el servicio upstream.

Cómo resolver: la causa más común es una codificación de URL incorrecta — los caracteres + que aparecen de forma natural en el cuerpo base64 deben codificarse como %2B, no como %20. Algunos encoders (por ejemplo, url.QueryEscape de Go seguido de un reemplazo ingenuo + → %20) corrompen el base64 de esta manera. Use url.PathEscape en Go, o verifique que su encoder produzca %2B para el + literal.

PUB_REQUEST_BODY_INVALID (400)

El cuerpo JSON no cumple con el esquema (campo faltante, tipo incorrecto, o clientId no es un UUID v4). Revise details.violations para identificar el o los campos con problemas.

PUB_CERT_NOT_YET_VALID (401)

La fecha notBefore del certificado está en el futuro.

Cómo resolver: sincronice el reloj del sistema mediante NTP. Si la fecha es genuinamente futura, espere o solicite un nuevo certificado.

PUB_CERT_EXPIRED (401)

La fecha notAfter del certificado ha vencido.

Cómo resolver: solicite un nuevo certificado al soporte y rótelo en su integración.

PUB_CERT_NOT_REGISTERED (401)

La huella digital SHA-256 del certificado no está en el registro — nunca fue registrado o ha sido revocado.

Cómo resolver: calcule la huella digital local con openssl x509 -noout -fingerprint -sha256 -in cert.pem y compárela con la huella digital recibida durante el onboarding. Si coinciden, contacte al soporte — el certificado puede haber sido revocado. Si difieren, contacte al soporte para registrar el nuevo certificado.

Nota: los certificados revocados actualmente se reportan como PUB_CERT_NOT_REGISTERED. Un código dedicado PUB_CERT_REVOKED puede introducirse en una versión futura; hasta entonces, este código cubre ambos casos y el equipo de soporte distingue.

PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)

El certificado es válido y está registrado, pero no está vinculado a la cuenta propietaria del clientId proporcionado.

Cómo resolver: verifique que está usando el par certificado/clientId correcto para este entorno.

PUB_INVALID_CREDENTIALS (401)

El clientId no es reconocido, o el clientSecret no coincide. La plataforma deliberadamente no diferencia entre estos casos (resistencia a la enumeración).

Cómo resolver: verifique tanto el clientId como el clientSecret. Si el problema persiste, contacte al soporte con el errorId de la respuesta.

PUB_AUTH_UPSTREAM_UNAVAILABLE (503)

El servicio de autenticación upstream está temporalmente inaccesible.

Cómo resolver: reintente con retroceso exponencial (por ejemplo, 1s, 2s, 4s). Si el problema persiste por más de un minuto, contacte al soporte.

PUB_AUTH_UPSTREAM_ERROR (502)

El upstream devolvió un error no reconocido.

Cómo resolver: contacte al soporte con el errorId — este código indica un problema del lado del servidor que investigaremos.

Notas de migración (para integraciones existentes)

Si su integración verifica directamente los códigos antiguos PUB_INVALID_CERTIFICATE o PUB_TOKEN_GENERATION_FAILED, han sido renombrados:

Código anteriorNuevo(s) código(s)
PUB_INVALID_CERTIFICATEPUB_CERT_MALFORMED_PEM (solo fallos de análisis PEM — otros problemas de certificado ahora tienen códigos dedicados)
PUB_TOKEN_GENERATION_FAILEDPUB_AUTH_UPSTREAM_UNAVAILABLE (503) para indisponibilidad transitoria, PUB_AUTH_UPSTREAM_ERROR (502) para errores upstream no reconocidos

PUB_INVALID_CREDENTIALS se mantiene, pero con un significado más acotado — ahora solo se activa cuando el clientId o el clientSecret es incorrecto, nunca como comodín para cualquier 401 del servicio de autenticación.

Solución de problemas

"Mi certificado funciona con openssl pero la API devuelve PUB_CERT_MALFORMED_PEM"

El archivo PEM en disco está bien — el problema está en el transporte. Lo más común es el bug de codificación de URL + → %20. Imprima el valor exacto del encabezado que envía su cliente, decodifíquelo y compárelo byte a byte con el PEM original. El base64 debe preservar cada + literal como %2B.

"Recibo ocasionalmente PUB_CERT_NOT_YET_VALID"

El reloj del sistema se ha desviado más allá de la fecha notBefore de su certificado. Configure NTP y asegúrese de que el reloj del host esté a pocos segundos del UTC. Los contenedores pueden heredar la deriva del reloj del host — verifique con date -u dentro del contenedor.

"Recibo PUB_CERT_NOT_REGISTERED tras rotar el certificado"

Calcule la huella digital del nuevo certificado y confirme que fue registrado: openssl x509 -noout -fingerprint -sha256 -in new-cert.pem. Si la huella digital difiere de la que tenemos registrada, solicite el registro al soporte antes de realizar la transición.

Mejores Prácticas

Próximos Pasos

En esta página