Avistadocs
Guias de Integração

Autenticação

Visão Geral

A API Avista utiliza autenticação baseada em OAuth 2.0 com certificados X.509 (mTLS). Este modelo de segurança em camadas garante que apenas clientes autorizados com certificados válidos possam acessar a API.

Por que mTLS?

O mTLS (mutual TLS) oferece segurança superior comparado a tokens simples:

  • Autenticação mútua: Tanto o cliente quanto o servidor se autenticam
  • Não-repúdio: Certificados vinculados à conta garantem rastreabilidade
  • Proteção contra roubo de credenciais: Mesmo com clientId/clientSecret vazados, o atacante precisaria do certificado

Pré-requisitos

Antes de iniciar, você precisará:

Obtenha seu certificado cliente através do portal Avista. O certificado deve estar no formato PEM e será vinculado à sua conta.

Solicite suas credenciais (clientId e clientSecret) no painel administrativo.

Configure seu ambiente para enviar o certificado no header X-SSL-Client-Cert.

O certificado X.509 deve estar vinculado à sua conta antes de ser utilizado. Certificados não vinculados serão rejeitados mesmo que válidos.

Endpoint de Autenticação

POST /api/auth/token

Gera um token JWT de acesso válido por 30 minutos (1800 segundos).

O certificado X.509 deve ser enviado URL-encoded no header X-SSL-Client-Cert. O sistema valida o fingerprint SHA256 do certificado contra os registros vinculados à conta.

Request

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"
  }'

Response (201 Created)

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

Exemplo Prático: Node.js

Instalação

npm install axios

Código Completo

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

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

// Configuração da requisição
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
  }
};

// Fazer requisição
async function getToken() {
  try {
    const response = await axios(config);

    console.log('Token obtido com sucesso!');
    console.log('Expira em:', response.data.expires_in, 'segundos');

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

getToken();

Exemplo Prático: Python

Instalação

pip install requests

Código Completo

import os
import requests
import urllib.parse

# Carregar e codificar certificado
with open('client-cert.pem', 'r') as f:
    certificate = f.read()
    encoded_cert = urllib.parse.quote(certificate)

# Configuração da requisição
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')
}

# Fazer requisição
try:
    response = requests.post(url, json=payload, headers=headers)
    response.raise_for_status()

    data = response.json()

    print('Token obtido com sucesso!')
    print(f"Expira em: {data['expires_in']} segundos")

except requests.exceptions.RequestException as e:
    print(f'Erro ao obter token: {e}')
    if hasattr(e.response, 'text'):
        print(f'Resposta: {e.response.text}')

Utilizando o Token

Após obter o token, inclua-o no header Authorization de todas as requisições:

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

Renovação de Token

Tokens expiram após 30 minutos. Implemente lógica de renovação automática em sua aplicação para evitar interrupções.

Estratégia Recomendada

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

  async getValidToken() {
    // Verifica se o token ainda é válido (com margem de 30 segundos)
    if (this.token && this.expiresAt && Date.now() < this.expiresAt - 30000) {
      return this.token;
    }

    // Renova o 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;
  }
}

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

// Em qualquer requisição
const token = await tokenManager.getValidToken();

Validação do Certificado

O sistema realiza as seguintes validações no certificado:

  1. Formato PEM válido: O certificado deve estar no formato PEM e URL-encoded
  2. Vinculação à conta: O fingerprint SHA256 do certificado deve estar registrado e vinculado à sua conta
  3. Correspondência de credenciais: O certificado deve pertencer à mesma conta das credenciais OAuth

Certificados não vinculados ou vinculados a outra conta serão rejeitados, mesmo que tecnicamente válidos.

Respostas de erro

Todos os erros retornados por POST /api/auth/token seguem o mesmo formato:

{
  "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 estável legível por máquina — faça o branch com base neste campo.
  • message: descrição técnica resumida (em inglês, voltada para logs).
  • userMessage: descrição voltada ao usuário final (em inglês).
  • details.hint: orientação prática para desenvolvedores integrando a API.
  • errorId: identificador único da requisição — informe-o ao contatar o suporte.

Referência de códigos de erro

PUB_CERT_HEADER_MISSING (400)

O cabeçalho X-SSL-Client-Cert não foi enviado na requisição.

Como resolver: inclua o cabeçalho com o certificado PEM codificado em URL. Se você estiver atrás de um gateway que faz terminação TLS (NGINX/ALB), confirme que ele está configurado para encaminhar $ssl_client_escaped_cert como X-SSL-Client-Cert.

PUB_CERT_MALFORMED_PEM (400)

O certificado não pôde ser analisado no gateway ou no serviço upstream.

Como resolver: a causa mais comum é codificação de URL incorreta — os caracteres + que aparecem naturalmente no corpo base64 devem ser codificados como %2B, não %20. Alguns encoders (por exemplo, url.QueryEscape do Go seguido de uma substituição ingênua + → %20) corrompem o base64 dessa forma. Use url.PathEscape no Go, ou verifique se seu encoder produz %2B para + literal.

PUB_REQUEST_BODY_INVALID (400)

O corpo JSON não satisfez o esquema (campo ausente, tipo errado, ou clientId não é um UUID v4). Verifique details.violations para identificar o(s) campo(s) com problema.

PUB_CERT_NOT_YET_VALID (401)

A data notBefore do certificado está no futuro.

Como resolver: sincronize o relógio do sistema via NTP. Se a data realmente estiver no futuro, aguarde ou solicite um novo certificado.

PUB_CERT_EXPIRED (401)

A data notAfter do certificado expirou.

Como resolver: solicite um novo certificado ao suporte e substitua-o na sua integração.

PUB_CERT_NOT_REGISTERED (401)

A impressão digital SHA-256 do certificado não está no registro — ele nunca foi registrado ou foi revogado.

Como resolver: calcule a impressão digital local com openssl x509 -noout -fingerprint -sha256 -in cert.pem e compare com a impressão digital recebida no onboarding. Se coincidirem, contate o suporte — o certificado pode ter sido revogado. Se diferirem, contate o suporte para registrar o novo certificado.

Nota: certificados revogados atualmente são reportados como PUB_CERT_NOT_REGISTERED. Um código dedicado PUB_CERT_REVOKED pode ser introduzido em uma release futura; até lá, este código cobre ambos os casos e a equipe de suporte distingue.

PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)

O certificado é válido e registrado, mas não está vinculado à conta que possui o clientId informado.

Como resolver: verifique se você está usando o par certificado/clientId correto para este ambiente.

PUB_INVALID_CREDENTIALS (401)

O clientId não é reconhecido, ou o clientSecret não corresponde. A plataforma deliberadamente não diferencia esses casos (resistência a enumeração).

Como resolver: verifique tanto o clientId quanto o clientSecret. Se o problema persistir, contate o suporte com o errorId presente na resposta.

PUB_AUTH_UPSTREAM_UNAVAILABLE (503)

O serviço de autenticação upstream está temporariamente inacessível.

Como resolver: tente novamente com backoff exponencial (por exemplo, 1s, 2s, 4s). Se o problema persistir por mais de um minuto, contate o suporte.

PUB_AUTH_UPSTREAM_ERROR (502)

O upstream retornou um erro não reconhecido.

Como resolver: contate o suporte com o errorId — esse código indica um problema no servidor que investigaremos.

Notas de migração (para integrações existentes)

Se sua integração verifica diretamente os códigos antigos PUB_INVALID_CERTIFICATE ou PUB_TOKEN_GENERATION_FAILED, eles foram renomeados:

Código antigoNovo(s) código(s)
PUB_INVALID_CERTIFICATEPUB_CERT_MALFORMED_PEM (apenas falhas de análise PEM — outros problemas de certificado agora têm códigos dedicados)
PUB_TOKEN_GENERATION_FAILEDPUB_AUTH_UPSTREAM_UNAVAILABLE (503) para indisponibilidade transitória, PUB_AUTH_UPSTREAM_ERROR (502) para erros upstream não reconhecidos

PUB_INVALID_CREDENTIALS é mantido, mas com significado mais estrito — agora dispara apenas quando o clientId ou clientSecret está errado, nunca como coringa para qualquer 401 do serviço de autenticação.

Resolução de problemas

"Meu certificado funciona com openssl mas a API retorna PUB_CERT_MALFORMED_PEM"

O arquivo PEM em disco está correto — o problema está no transporte. Na maioria dos casos, é o bug de codificação de URL + → %20. Imprima o valor exato do cabeçalho enviado pelo seu cliente, decodifique-o e compare byte a byte com o PEM original. O base64 deve preservar cada + literal como %2B.

"Recebo ocasionalmente PUB_CERT_NOT_YET_VALID"

O relógio do sistema derivou além da data notBefore do seu certificado. Configure o NTP e garanta que o relógio do host esteja com poucos segundos de diferença do UTC. Contêineres podem herdar a deriva de relógio do host — verifique com date -u dentro do contêiner.

"Recebo PUB_CERT_NOT_REGISTERED após rotacionar o certificado"

Calcule a impressão digital do novo certificado e confirme que ele foi registrado: openssl x509 -noout -fingerprint -sha256 -in new-cert.pem. Se a impressão digital diferir do que temos registrado, solicite o registro ao suporte antes de realizar a transição.

Boas Práticas

Próximos Passos

Nesta página