Pular para o conteúdo principal

Obtenha um token

POST https://api.sdb.lbpay.com.br/v2/oauth/token

O corpo é application/x-www-form-urlencoded com três campos, mais um header DPoP. Não é JSON.

grant_type=client_credentials
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion=<jwt>
DPoP: <jwt>

Onde entra cada chave​

A privada nunca sai da sua infraestrutura. Ela só assina: a client_assertion e cada prova DPoP. Não vai em header, nem em corpo, nem para nós.

A pública você registra uma vez no portal, e ela também viaja em toda requisição, dentro do header DPoP, no campo jwk. Isso é exigência da RFC 9449 e não é vazamento: pública é pública. Ela serve para verificarmos a prova, e conferimos que o thumbprint dela é igual ao cnf.jkt gravado no token na emissão. Assinar com outra chave dá 401.

Você não precisa manter um arquivo separado com a pública em JWK: ela é derivada da privada em uma linha.

A client assertion​

Um JWT assinado em ES256 com sua privada:

header:  { "alg": "ES256", "kid": "<kid>" }
payload: { "iss": "<client_id>",
"sub": "<client_id>",
"aud": "https://api.sdb.lbpay.com.br/v2/oauth/token",
"iat": <agora>,
"exp": <agora + 60>,
"jti": "<uuid>" }

O aud precisa ser o endpoint de token, byte a byte. Uma assertion emitida para outro ambiente é recusada.

A prova DPoP do endpoint de token​

Mesmo formato da prova por requisição, sem ath, qh e bh, porque ainda não existe token nem corpo para amarrar:

header:  { "typ": "dpop+jwt", "alg": "ES256", "jwk": { ...sua chave pública... } }
payload: { "htm": "POST",
"htu": "https://api.sdb.lbpay.com.br/v2/oauth/token",
"iat": <agora>,
"jti": "<uuid>" }

Exemplo completo​

// Fluxo completo: obtém o token e faz uma chamada assinada. Só biblioteca padrão.
import { createHash, createPrivateKey, createPublicKey, createSign, randomUUID } from 'node:crypto'
import { readFileSync } from 'node:fs'

const BASE = process.env.LBPAY_BASE ?? 'https://seamless-v2.sdb.lbpay.com.br'
const CLIENT_ID = process.env.LBPAY_CLIENT_ID ?? 'cli-demo'
const KID = process.env.LBPAY_KID ?? 'kid-demo'

const privateKey = createPrivateKey(readFileSync('private.pem', 'utf8'))
// A pública sai da privada: você não precisa manter um segundo arquivo.
const publicJwk = createPublicKey(privateKey).export({ format: 'jwk' })

const b64u = (b) => Buffer.from(b).toString('base64url')
const sha = (s) => b64u(createHash('sha256').update(s).digest())
const now = () => Math.floor(Date.now() / 1000)

function jws(header, payload) {
const h = b64u(JSON.stringify(header))
const p = b64u(JSON.stringify(payload))
// A saída default do Node (DER) é aceita; R‖S também. Use o que sua lib emitir.
const signature = createSign('sha256').update(`${h}.${p}`).sign(privateKey)
return `${h}.${p}.${b64u(signature)}`
}

// ── 1. token ───────────────────────────────────────────────────────────────────────
async function getToken() {
const url = `${BASE}/oauth/token`
const iat = now()

const clientAssertion = jws(
{ alg: 'ES256', kid: KID },
{ iss: CLIENT_ID, sub: CLIENT_ID, aud: url, iat, exp: iat + 60, jti: randomUUID() },
)
// Versão curta: aqui ainda não há token, query nem corpo para amarrar.
const proof = jws(
{ typ: 'dpop+jwt', alg: 'ES256', jwk: publicJwk },
{ htm: 'POST', htu: url, iat, jti: randomUUID() },
)

const res = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded', dpop: proof },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: clientAssertion,
}),
})
if (!res.ok) throw new Error(`token ${res.status}: ${await res.text()}`)
return (await res.json()).access_token
}

Cada aba é executada contra um servidor que confere as duas provas como a produção confere: assinatura, typ, htu, ath, bh, a janela do iat e que nenhum componente privado vaze no jwk. Documentação que ninguém rodou é documentação que não funciona.

Validade e renovação​

O token vale no máximo 60 segundos, para qualquer perfil. Renove entre 35 e 45 s, uma renovação por vez (singleflight). Não dispare uma renovação por requisição.

Por que 60 segundos

O token carrega a credencial assinada inteira, então o servidor de recurso não precisa consultar banco no caminho quente. Sessenta segundos é o que impede uma revogação de ficar pendurada: passado esse tempo, o token acabou, aconteça o que acontecer.

jti de uso único​

Cada jti é de uso único, tanto na assertion quanto na prova. Reaproveitar um vale API_CREDENTIAL_PROOF_REPLAY, então gere um UUID novo por prova.