A arquitetura em uma imagem
Node.js pode terminar TLS diretamente, mas em produção quase todo mundo coloca um proxy reverso (NGINX) na frente para cuidar do TLS e do mTLS, deixando a aplicação focada na lógica. É esse o desenho que vamos seguir, porque é o mais comum e o mais robusto:
Se você ainda não tem o mTLS configurado no NGINX, o primeiro artigo desta série cobre isso em detalhe — aqui vamos assumir que o handshake já acontece e focar no lado Node.
Terminando o mTLS no proxy
No bloco server do NGINX, além de exigir o certificado do cliente, você repassa o resultado e os dados para o backend via cabeçalhos:
location / {
proxy_pass http://127.0.0.1:3000;
# resultado da validação feita pelo NGINX
proxy_set_header X-SSL-Verify $ssl_client_verify;
# dados do titular (Subject DN)
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
# certificado completo, caso o Node precise reprocessar
proxy_set_header X-SSL-Cert $ssl_client_escaped_cert;
}
Lendo o certificado no Express
No Node, um middleware lê os cabeçalhos e barra logo de cara quem não passou na validação do NGINX:
const express = require("express");
const app = express();
function autenticarCertificado(req, res, next) {
const verify = req.headers["x-ssl-verify"];
// o NGINX já validou a cadeia; se não passou, nem seguimos
if (verify !== "SUCCESS") {
return res.status(401).json({ erro: "Certificado não reconhecido" });
}
const subject = req.headers["x-ssl-subject"];
// aqui vem a parte trabalhosa: extrair CPF/CNPJ e nome
// dos OIDs proprietários da ICP-Brasil dentro do certificado
req.titular = parseIcpBrasil(subject, req.headers["x-ssl-cert"]);
if (!req.titular) {
return res.status(401).json({ erro: "Dados do certificado inválidos" });
}
next();
}
app.post("/login", autenticarCertificado, (req, res) => {
// req.titular já tem CPF/CNPJ e nome
const sessao = criarSessao(req.titular);
res.json({ ok: true, sessao });
});
Parece simples — e o esqueleto é. O peso está escondido dentro de duas funções: parseIcpBrasil() e a revalidação. É onde a maior parte do tempo de desenvolvimento vai.
O passo que não pode faltar: validar (de novo)
O NGINX valida a cadeia no handshake, mas há duas coisas que você ainda precisa tratar do lado da aplicação:
- Revogação. O NGINX, por padrão, não checa CRL/OCSP da ICP-Brasil. Um certificado pode estar na cadeia correta e ainda assim ter sido revogado. Ignorar isso é aceitar credenciais que deveriam estar bloqueadas.
- Parsing dos dados do titular. Extrair CPF, CNPJ e nome exige ler as extensões de OID proprietário da ICP-Brasil (o arco 2.16.76.1.3.*), onde os campos vêm concatenados por posição. É o mesmo desafio que existe em qualquer linguagem — e onde bugs sutis (um CPF deslocado por um caractere) passam despercebidos.
Criando a sessão
Com o titular validado e os dados extraídos, o resto é o seu fluxo de sempre: gerar um token de sessão (JWT, cookie assinado, o que você já usa), associar ao CPF/CNPJ e seguir. A parte específica de certificado termina aqui — daqui para frente é a sua aplicação como qualquer outra.
O ponto a reter: o "difícil" do login por certificado não é o Express nem a sessão — isso você já sabe fazer. O difícil é tudo que acontece antes de req.titular existir de forma confiável: o mTLS, a revogação e o parsing. E, ao contrário do resto da sua aplicação, essa parte depende de uma norma externa que muda sem te avisar.
O que fica de manutenção
Depois de no ar, o código Node praticamente não muda — mas o ecossistema ICP-Brasil embaixo dele, sim:
- A cadeia de ACs que o NGINX confia precisa acompanhar as adições e renovações da ICP-Brasil, senão usuários legítimos passam a falhar no handshake.
- A verificação de revogação precisa de CRLs atualizadas e tratamento de indisponibilidade dos endpoints.
- O parseIcpBrasil() precisa cobrir cada nova variação de certificado que aparecer entre os seus clientes.
Nenhuma dessas tarefas é sobre Node ou Express. São sobre manter uma camada de identidade viva, sensível a segurança, atrelada a uma norma que não é sua — indefinidamente.
O login por certificado sem a parte difícil
Com o IHub-Auth, seu backend Node recebe CPF, CNPJ e nome do titular já validados via verificação server-to-server — sem terminar mTLS, sem checar CRL, sem fazer parsing de OID. A cadeia ICP-Brasil completa fica atualizada do nosso lado. Você inclui um script, configura o token e trata só a sessão, que é a parte que você já domina.