Developers/Documentação da API/OAuth 2.0 — Guia Completo

OAuth 2.0 — Guia Completo

Guia didático passo a passo para integrar sua aplicação com o Bunto ERP usando OAuth 2.0. Exemplos em Python, JavaScript, PHP, Java, C# e Go.

13/02/202618 min de leitura18 visualizações

Conecte sua aplicação ao Bunto ERP e acesse dados de outros usuários com segurança. Este guia explica passo a passo como funciona o OAuth 2.0 no Bunto, desde a criação do aplicativo até a primeira chamada à API.

O que é OAuth 2.0?

OAuth 2.0 é um protocolo de autorização que permite que aplicações de terceiros acessem dados do Bunto ERP em nome de um usuário, sem que ele precise compartilhar sua senha.

Quando usar OAuth?

CenárioMétodo recomendado
Sua própria empresa quer integrar sistemas (ERP ↔ loja, ERP ↔ automação)Token de API — mais simples, acesso direto
Você está construindo uma aplicação que outros usuários do Bunto vão conectarOAuth 2.0 — cada usuário autoriza individualmente
SaaS que atende múltiplas empresas do BuntoOAuth 2.0 — cada empresa autoriza o acesso
Plugin ou extensão para o ecossistema BuntoOAuth 2.0 — publicável para qualquer empresa

Resumo: Se a integração é só para sua empresa, use Token de API. Se outros usuários/empresas vão se conectar, use OAuth 2.0.


Visão Geral do Fluxo

O OAuth 2.0 do Bunto usa o Authorization Code Grant (RFC 6749), o método mais seguro para aplicações web. O fluxo completo tem 5 passos:

code
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Sua Aplicação │ │ Bunto ERP │ │ API Bunto v1 │ │ (Client) │ │ (Frontend) │ │ (Resource) │ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │ │ │ 1. │──── Redireciona usuário ───►│ │ │ /oauth/autorizar/ │ │ │ ?client_id=xxx │ │ │ &scope=produtos:read │ │ │ &state=random123 │ │ │ │ │ 2. │ │ Tela de Consentimento │ │ │ "App X quer acessar..." │ │ │ [Negar] [Autorizar] │ │ │ │ 3. │◄── Redireciona de volta ────│ │ │ /callback?code=xxx │ │ │ &state=random123 │ │ │ │ │ 4. │──── POST /oauth/token/ ─────────────────────────────────►│ │ code + client_secret │ │ │◄──── access_token ──────────────────────────────────────│ │ + refresh_token │ │ │ │ │ 5. │──── GET /v1/produtos/ ──────────────────────────────────►│ │ Authorization: Bearer │ │ │◄──── Dados do usuário ──────────────────────────────────│ │ │ │

Explicando cada passo:

  1. Sua aplicação redireciona o usuário para a tela de consentimento do Bunto
  2. O usuário vê quais permissões sua aplicação está pedindo e decide autorizar ou negar
  3. O Bunto redireciona de volta para sua aplicação com um código temporário
  4. Sua aplicação troca o código por tokens de acesso (server-to-server, seguro)
  5. Sua aplicação usa o access token para acessar a API em nome do usuário

Passo 1: Registrar seu Aplicativo

Antes de começar, você precisa registrar sua aplicação no Bunto ERP. Isso gera as credenciais (client_id e client_secret) necessárias para o fluxo OAuth.

Onde criar

  1. Faça login no painel do Bunto ERP
  2. No menu lateral, acesse Integrações
  3. Clique na aba Aplicativos OAuth
  4. Clique no botão Novo Aplicativo

O que preencher

CampoDescriçãoExemplo
NomeNome exibido na tela de consentimento"Minha Loja Virtual"
DescriçãoO que sua aplicação faz (o usuário verá isso)"Sincroniza pedidos e estoque com sua loja"
Redirect URIsURLs de callback da sua aplicação (HTTPS obrigatório em produção)https://meuapp.com/callback
EscoposPermissões máximas que o app pode solicitarprodutos:read, vendas:read, vendas:write

Credenciais geradas

Ao salvar, você receberá duas credenciais:

code
client_id: bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB client_secret: bnt_secret_9FO4BBQKu90FwCnis8neCfePD7JpU7pmqzYFNrRitmLxg9srQ...

Atenção: O client_secret é exibido apenas uma vez. Copie e armazene em local seguro imediatamente. Nunca exponha o secret no código frontend, em repositórios públicos ou em logs.

Sobre os escopos

Os escopos definem o que sua aplicação pode fazer com os dados do usuário. Cada escopo segue o formato modulo:acao:

code
produtos:read → Ler produtos vendas:read → Ler vendas vendas:write → Criar e atualizar vendas clientes:delete → Excluir clientes

Ações disponíveis:

AçãoMétodos HTTPO que permite
readGET, HEAD, OPTIONSConsultar e listar dados
writePOST, PUT, PATCHCriar e atualizar registros
deleteDELETEExcluir registros

Princípio do menor privilégio: solicite apenas os escopos que sua aplicação realmente precisa. Uma integração de vitrine que só exibe produtos precisa apenas de produtos:read — não peça vendas:write se não vai criar pedidos.


Passo 2: Redirecionar o Usuário

Quando um usuário quer conectar sua conta do Bunto à sua aplicação, redirecione-o para a tela de consentimento:

code
https://erp.bunto.com.br/oauth/autorizar/ ?client_id=bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB &redirect_uri=https://meuapp.com/callback &scope=produtos:read,vendas:read,vendas:write &state=aB3dE5fG7hI9jK1L &response_type=code

Parâmetros obrigatórios

ParâmetroDescrição
client_idID público do seu aplicativo (começa com bnt_app_)
redirect_uriURL de callback — deve ser exatamente igual à cadastrada no aplicativo
scopeEscopos solicitados, separados por vírgula
stateString aleatória para proteção contra CSRF — sempre gere um valor único
response_typeDeve ser code

Sobre o parâmetro state

O state é uma string aleatória que você gera antes de redirecionar o usuário. Quando o Bunto redirecionar de volta, ele devolve o mesmo state. Você deve comparar os dois valores para garantir que ninguém interceptou a requisição.

Exemplos de código

Python (Flask)

python
import secrets from flask import Flask, redirect, session app = Flask(__name__) app.secret_key = "sua_chave_secreta" CLIENT_ID = "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB" REDIRECT_URI = "https://meuapp.com/callback" SCOPES = "produtos:read,vendas:read" @app.route("/conectar-bunto") def conectar_bunto(): # Gerar state aleatório para proteção CSRF state = secrets.token_urlsafe(16) session["oauth_state"] = state url = ( f"https://erp.bunto.com.br/oauth/autorizar/" f"?client_id={CLIENT_ID}" f"&redirect_uri={REDIRECT_URI}" f"&scope={SCOPES}" f"&state={state}" f"&response_type=code" ) return redirect(url)

JavaScript (Node.js + Express)

javascript
const crypto = require("crypto"); const express = require("express"); const app = express(); const CLIENT_ID = "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB"; const REDIRECT_URI = "https://meuapp.com/callback"; const SCOPES = "produtos:read,vendas:read"; app.get("/conectar-bunto", (req, res) => { // Gerar state aleatório para proteção CSRF const state = crypto.randomBytes(16).toString("hex"); req.session.oauthState = state; const url = new URL("https://erp.bunto.com.br/oauth/autorizar/"); url.searchParams.set("client_id", CLIENT_ID); url.searchParams.set("redirect_uri", REDIRECT_URI); url.searchParams.set("scope", SCOPES); url.searchParams.set("state", state); url.searchParams.set("response_type", "code"); res.redirect(url.toString()); });

PHP

php
<?php session_start(); $client_id = "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB"; $redirect_uri = "https://meuapp.com/callback"; $scopes = "produtos:read,vendas:read"; // Gerar state aleatório para proteção CSRF $state = bin2hex(random_bytes(16)); $_SESSION["oauth_state"] = $state; $params = http_build_query([ "client_id" => $client_id, "redirect_uri" => $redirect_uri, "scope" => $scopes, "state" => $state, "response_type" => "code", ]); header("Location: https://erp.bunto.com.br/oauth/autorizar/?{$params}"); exit;

C# (.NET)

csharp
using Microsoft.AspNetCore.Mvc; using System.Security.Cryptography; [Route("conectar-bunto")] public IActionResult ConectarBunto() { var state = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16)); HttpContext.Session.SetString("oauth_state", state); var url = "https://erp.bunto.com.br/oauth/autorizar/" + $"?client_id=bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB" + $"&redirect_uri=https://meuapp.com/callback" + $"&scope=produtos:read,vendas:read" + $"&state={Uri.EscapeDataString(state)}" + $"&response_type=code"; return Redirect(url); }

Passo 3: Tela de Consentimento

Quando o usuário é redirecionado, o Bunto exibe uma tela de consentimento com:

  • Nome e descrição da sua aplicação
  • Lista de permissões que você está solicitando (escopos)
  • Dados do usuário conectado (nome, e-mail, empresa)
  • Botões Autorizar e Negar

O design da tela segue a identidade visual do Bunto com fundo limpo, logo centralizado e informações claras sobre o que está sendo autorizado. No rodapé, links para a Política de Privacidade e Termos de Uso.

Se o usuário autorizar

O Bunto redireciona de volta para sua redirect_uri com dois parâmetros:

https://meuapp.com/callback?code=bnt_code_U62uP-YOFeaXrmAh5s&state=aB3dE5fG7hI9jK1L
ParâmetroDescrição
codeCódigo de autorização temporário (uso único, expira em 10 minutos)
stateO mesmo valor que você enviou — valide-o!

Se o usuário negar

https://meuapp.com/callback?error=access_denied&error_description=O+usuário+negou+o+acesso&state=aB3dE5fG7hI9jK1L

Se houver erro nos parâmetros

https://meuapp.com/callback?error=invalid_scope&error_description=Escopo+inválido&state=aB3dE5fG7hI9jK1L

Erros possíveis:

ErroDescrição
invalid_requestParâmetros faltando ou inválidos
unauthorized_clientAplicativo desativado
invalid_scopeEscopos inválidos ou não permitidos para o app
unsupported_response_typeApenas code é suportado
access_deniedO usuário clicou em "Negar"

Passo 4: Trocar o Código por Tokens

Agora que você tem o código de autorização, troque-o por tokens de acesso. Essa chamada é server-to-server — nunca faça do frontend!

Endpoint

code
POST https://api-backend.bunto.com.br/integracoes/oauth/token/ Content-Type: application/x-www-form-urlencoded

Parâmetros

ParâmetroValor
grant_typeauthorization_code
codeO código recebido no callback
client_idSeu bnt_app_xxx
client_secretSeu bnt_secret_xxx
redirect_uriA mesma URI usada no passo 2

Resposta de sucesso (200)

json
{ "access_token": "bnt_oat_9q-CvruvQ-zY5xh0HOKfxbV0KYUEZE1j...", "token_type": "Bearer", "expires_in": 14400, "refresh_token": "bnt_ort_5ZzC996ed3F84UkExMcvw8K37znqSOlv...", "scope": "produtos:read,vendas:read" }
CampoDescrição
access_tokenToken para acessar a API (prefixo bnt_oat_, válido por 4 horas)
refresh_tokenToken para renovar o access token (prefixo bnt_ort_, válido por 30 dias)
token_typeSempre Bearer
expires_inSegundos até expirar (14400 = 4 horas)
scopeEscopos efetivamente concedidos

Exemplos de código

cURL

bash
curl -X POST https://api-backend.bunto.com.br/integracoes/oauth/token/ \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=bnt_code_U62uP-YOFeaXrmAh5s" \ -d "client_id=bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB" \ -d "client_secret=bnt_secret_9FO4BBQKu90FwCnis8ne..." \ -d "redirect_uri=https://meuapp.com/callback"

Python

python
import requests TOKEN_URL = "https://api-backend.bunto.com.br/integracoes/oauth/token/" CLIENT_ID = "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB" CLIENT_SECRET = os.environ["BUNTO_CLIENT_SECRET"] @app.route("/callback") def callback(): # 1. Validar state (proteção CSRF) state_recebido = request.args.get("state") if state_recebido != session.get("oauth_state"): return "Erro: state inválido (possível ataque CSRF)", 400 # 2. Verificar se houve erro error = request.args.get("error") if error: descricao = request.args.get("error_description", "Erro desconhecido") return f"Autorização negada: {descricao}", 400 # 3. Trocar código por tokens code = request.args.get("code") resposta = requests.post(TOKEN_URL, data={ "grant_type": "authorization_code", "code": code, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "redirect_uri": "https://meuapp.com/callback", }) if resposta.status_code != 200: return f"Erro ao obter token: {resposta.json()}", 400 tokens = resposta.json() # 4. Armazenar tokens com segurança (banco de dados criptografado) salvar_tokens_do_usuario( access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], expires_in=tokens["expires_in"], scope=tokens["scope"], ) return "Conectado ao Bunto ERP com sucesso!"

JavaScript (Node.js)

javascript
const axios = require("axios"); const TOKEN_URL = "https://api-backend.bunto.com.br/integracoes/oauth/token/"; const CLIENT_ID = "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB"; const CLIENT_SECRET = process.env.BUNTO_CLIENT_SECRET; app.get("/callback", async (req, res) => { // 1. Validar state (proteção CSRF) if (req.query.state !== req.session.oauthState) { return res.status(400).send("Erro: state inválido"); } // 2. Verificar se houve erro if (req.query.error) { return res.status(400).send(`Negado: ${req.query.error_description}`); } // 3. Trocar código por tokens const { data: tokens } = await axios.post(TOKEN_URL, new URLSearchParams({ grant_type: "authorization_code", code: req.query.code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: "https://meuapp.com/callback", })); // 4. Armazenar tokens com segurança await salvarTokens(tokens.access_token, tokens.refresh_token); res.send("Conectado ao Bunto ERP com sucesso!"); });

PHP

php
<?php // 1. Validar state if ($_GET["state"] !== $_SESSION["oauth_state"]) { die("Erro: state inválido (possível ataque CSRF)"); } // 2. Verificar erro if (isset($_GET["error"])) { die("Autorização negada: " . ($_GET["error_description"] ?? "Erro desconhecido")); } // 3. Trocar código por tokens $ch = curl_init("https://api-backend.bunto.com.br/integracoes/oauth/token/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ "grant_type" => "authorization_code", "code" => $_GET["code"], "client_id" => "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB", "client_secret" => getenv("BUNTO_CLIENT_SECRET"), "redirect_uri" => "https://meuapp.com/callback", ]), ]); $resposta = json_decode(curl_exec($ch), true); curl_close($ch); // 4. Armazenar tokens com segurança salvarTokens($resposta["access_token"], $resposta["refresh_token"]); echo "Conectado ao Bunto ERP com sucesso!";

Java (Spring Boot)

java
import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; @GetMapping("/callback") public String callback( @RequestParam String code, @RequestParam String state, HttpSession session ) { // 1. Validar state if (!state.equals(session.getAttribute("oauth_state"))) { return "Erro: state inválido"; } // 2. Trocar código por tokens RestTemplate rest = new RestTemplate(); var params = new LinkedMultiValueMap<String, String>(); params.add("grant_type", "authorization_code"); params.add("code", code); params.add("client_id", "bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB"); params.add("client_secret", System.getenv("BUNTO_CLIENT_SECRET")); params.add("redirect_uri", "https://meuapp.com/callback"); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var entity = new HttpEntity<>(params, headers); var resposta = rest.postForObject( "https://api-backend.bunto.com.br/integracoes/oauth/token/", entity, Map.class ); // 3. Armazenar tokens com segurança salvarTokens(resposta.get("access_token"), resposta.get("refresh_token")); return "Conectado ao Bunto ERP com sucesso!"; }

Go

go
package main import ( "encoding/json" "net/http" "net/url" "os" "strings" ) func callbackHandler(w http.ResponseWriter, r *http.Request) { // 1. Validar state state := r.URL.Query().Get("state") session, _ := store.Get(r, "session") if state != session.Values["oauth_state"] { http.Error(w, "Erro: state inválido", 400) return } // 2. Verificar erro if errParam := r.URL.Query().Get("error"); errParam != "" { http.Error(w, "Negado: "+r.URL.Query().Get("error_description"), 400) return } // 3. Trocar código por tokens dados := url.Values{ "grant_type": {"authorization_code"}, "code": {r.URL.Query().Get("code")}, "client_id": {"bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB"}, "client_secret": {os.Getenv("BUNTO_CLIENT_SECRET")}, "redirect_uri": {"https://meuapp.com/callback"}, } resp, _ := http.Post( "https://api-backend.bunto.com.br/integracoes/oauth/token/", "application/x-www-form-urlencoded", strings.NewReader(dados.Encode()), ) defer resp.Body.Close() var tokens map[string]interface{} json.NewDecoder(resp.Body).Decode(&tokens) // 4. Armazenar tokens com segurança salvarTokens(tokens["access_token"], tokens["refresh_token"]) w.Write([]byte("Conectado ao Bunto ERP com sucesso!")) }

Passo 5: Usar o Access Token

Com o access_token em mãos, você pode acessar a API v1 do Bunto em nome do usuário que autorizou:

bash
curl https://api-backend.bunto.com.br/v1/produtos/ \ -H "Authorization: Bearer bnt_oat_9q-CvruvQ-zY5xh0HOKfxbV0KYUEZE1j..."

O token OAuth funciona exatamente como um Token de API, mas com escopos granulares:

  • Requisições a módulos fora do escopo retornam 403 Forbidden
  • Requisições a módulos fora do plano da empresa retornam 403 Forbidden

Exemplo: listar produtos

Python

python
import requests ACCESS_TOKEN = "bnt_oat_9q-CvruvQ-zY5xh0HOKfxbV0KYUEZE1j..." headers = { "Authorization": f"Bearer {ACCESS_TOKEN}", "Content-Type": "application/json", } resposta = requests.get( "https://api-backend.bunto.com.br/v1/produtos/", headers=headers, ) dados = resposta.json() if dados["success"]: for produto in dados["data"]["resultados"]: print(f"{produto['nome']} — R$ {produto['preco']}") print(f"Total: {dados['data']['paginacao']['total_registros']}")

JavaScript

javascript
const ACCESS_TOKEN = "bnt_oat_9q-CvruvQ-zY5xh0HOKfxbV0KYUEZE1j..."; const resposta = await fetch("https://api-backend.bunto.com.br/v1/produtos/", { headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, "Content-Type": "application/json", }, }); const dados = await resposta.json(); if (dados.success) { dados.data.resultados.forEach((produto) => { console.log(`${produto.nome} — R$ ${produto.preco}`); }); console.log(`Total: ${dados.data.paginacao.total_registros}`); }

Renovando o Access Token

O access token expira em 4 horas. Quando expirar, use o refresh token para obter um novo sem precisar que o usuário autorize novamente:

Endpoint

code
POST https://api-backend.bunto.com.br/integracoes/oauth/token/ Content-Type: application/x-www-form-urlencoded

Parâmetros

ParâmetroValor
grant_typerefresh_token
refresh_tokenSeu bnt_ort_xxx
client_idSeu bnt_app_xxx
client_secretSeu bnt_secret_xxx

Resposta (200)

json
{ "access_token": "bnt_oat_novo_token_aqui...", "token_type": "Bearer", "expires_in": 14400, "scope": "produtos:read,vendas:read" }

O refresh_token permanece o mesmo e expira em 30 dias. Após expirar, o usuário precisa autorizar novamente.

Exemplo: renovação automática

Python

python
import requests import time class BuntoOAuth: """Cliente OAuth para a API v1 do Bunto ERP com renovação automática.""" TOKEN_URL = "https://api-backend.bunto.com.br/integracoes/oauth/token/" API_URL = "https://api-backend.bunto.com.br/v1" def __init__(self, access_token, refresh_token, client_id, client_secret): self.access_token = access_token self.refresh_token = refresh_token self.client_id = client_id self.client_secret = client_secret self.expires_at = time.time() + 14400 # 4 horas def _renovar_se_necessario(self): """Renova o token se estiver próximo de expirar (margem de 5 min).""" if time.time() > self.expires_at - 300: resposta = requests.post(self.TOKEN_URL, data={ "grant_type": "refresh_token", "refresh_token": self.refresh_token, "client_id": self.client_id, "client_secret": self.client_secret, }) if resposta.status_code == 200: dados = resposta.json() self.access_token = dados["access_token"] self.expires_at = time.time() + dados["expires_in"] def get(self, endpoint, **kwargs): """GET com renovação automática de token.""" self._renovar_se_necessario() return requests.get( f"{self.API_URL}/{endpoint.lstrip('/')}", headers={"Authorization": f"Bearer {self.access_token}"}, **kwargs, ) def post(self, endpoint, **kwargs): """POST com renovação automática de token.""" self._renovar_se_necessario() return requests.post( f"{self.API_URL}/{endpoint.lstrip('/')}", headers={ "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json", }, **kwargs, ) # Uso api = BuntoOAuth( access_token="bnt_oat_...", refresh_token="bnt_ort_...", client_id="bnt_app_...", client_secret=os.environ["BUNTO_CLIENT_SECRET"], ) # O token é renovado automaticamente quando necessário produtos = api.get("/produtos/").json()

JavaScript (Node.js)

javascript
class BuntoOAuth { constructor(accessToken, refreshToken, clientId, clientSecret) { this.accessToken = accessToken; this.refreshToken = refreshToken; this.clientId = clientId; this.clientSecret = clientSecret; this.expiresAt = Date.now() + 14400 * 1000; } async renovarSeNecessario() { if (Date.now() > this.expiresAt - 300000) { const resp = await fetch( "https://api-backend.bunto.com.br/integracoes/oauth/token/", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: this.refreshToken, client_id: this.clientId, client_secret: this.clientSecret, }), } ); if (resp.ok) { const dados = await resp.json(); this.accessToken = dados.access_token; this.expiresAt = Date.now() + dados.expires_in * 1000; } } } async get(endpoint) { await this.renovarSeNecessario(); const resp = await fetch( `https://api-backend.bunto.com.br/v1/${endpoint.replace(/^\//, "")}`, { headers: { Authorization: `Bearer ${this.accessToken}` } } ); return resp.json(); } } // Uso const api = new BuntoOAuth("bnt_oat_...", "bnt_ort_...", "bnt_app_...", process.env.BUNTO_CLIENT_SECRET); const produtos = await api.get("/produtos/");

Revogando Tokens

Para desconectar um usuário (logout, remoção de integração), revogue o token:

Endpoint

code
POST https://api-backend.bunto.com.br/integracoes/oauth/revoke/ Content-Type: application/x-www-form-urlencoded

Parâmetros

ParâmetroValor
tokenO token a ser revogado (access ou refresh)
client_idSeu bnt_app_xxx
client_secretSeu bnt_secret_xxx
token_type_hintaccess_token ou refresh_token (opcional)

Exemplo

bash
curl -X POST https://api-backend.bunto.com.br/integracoes/oauth/revoke/ \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=bnt_oat_9q-CvruvQ-zY5xh0HOKfxbV0KYUEZE1j..." \ -d "client_id=bnt_app_cWPeiCEfIqHKK1pxuSatycj93RrFOJzB" \ -d "client_secret=bnt_secret_9FO4BBQKu90FwCnis8ne..."

Este endpoint sempre retorna 200 OK (mesmo se o token não existir ou já estiver revogado), conforme RFC 7009. Isso evita que um atacante descubra quais tokens são válidos.

Revogação pelo usuário

Os usuários também podem revogar o acesso da sua aplicação a qualquer momento pelo painel do Bunto ERP, em Integrações → Aplicativos. Quando revogado, todos os tokens associados são invalidados imediatamente.


Validade e Segurança dos Tokens

TokenPrefixoValidadeArmazenamento
Código de autorizaçãobnt_code_10 minutos, uso únicoNão armazene — troque imediatamente
Access tokenbnt_oat_4 horasBanco de dados criptografado
Refresh tokenbnt_ort_30 diasBanco de dados criptografado

Por que os tokens expiram?

  • Access token (4h): Limita o dano em caso de vazamento. Um token roubado para de funcionar em até 4 horas.
  • Refresh token (30d): Permite renovação sem re-autorização, mas tem validade finita. Após 30 dias, o usuário precisa autorizar novamente.
  • Código de autorização (10min): Uso único e curtíssimo, apenas para trocar por tokens no server-to-server.

Todos os tokens são criptografados

O Bunto nunca armazena tokens em texto puro. Todos são criptografados com Fernet (criptografia simétrica) antes de serem gravados no banco de dados. A validação usa comparação constant-time (hmac.compare_digest) para prevenir timing attacks.


Tratamento de Erros

Erros no endpoint de token

json
{ "error": "invalid_grant", "error_description": "Código de autorização inválido ou expirado" }
ErroStatusCausaSolução
invalid_client401client_id ou client_secret incorretoVerifique as credenciais
invalid_grant400Código expirado, já usado, ou redirect_uri diferenteReinicie o fluxo OAuth
unsupported_grant_type400grant_type inválidoUse authorization_code ou refresh_token

Erros na API v1

StatusCausaExemplo de resposta
401Token expirado ou inválido{"success": false, "message": "Token inválido"}
403Escopo insuficiente{"success": false, "message": "Permissão negada", "errors": {"detail": ["Token não tem permissão para este recurso"]}}
403Módulo fora do plano{"success": false, "message": "Permissão negada", "errors": {"detail": ["Módulo \"vendas\" não disponível no plano da empresa"]}}
429Rate limit excedidoAguarde e tente novamente com backoff

Limites

RecursoLimite
Aplicativos por empresa5
Redirect URIs por aplicativo5
Código de autorização10 minutos, uso único
Access token4 horas
Refresh token30 dias
Rate limit (API v1)120 GET/min, 30 POST/min, 10 DELETE/min
Rate limit (autenticação)20 falhas por IP → bloqueio de 15 minutos

Boas Práticas de Segurança

1. Nunca exponha o client_secret

O secret deve ficar apenas no seu servidor. Nunca inclua em:

  • Código frontend (JavaScript no navegador, apps mobile)
  • Repositórios públicos no GitHub
  • Logs de aplicação
  • URLs ou query strings
python
# Correto — variável de ambiente CLIENT_SECRET = os.environ["BUNTO_CLIENT_SECRET"] # Errado — hardcoded CLIENT_SECRET = "bnt_secret_9FO4BBQKu90F..." # NUNCA faça isso!

2. Sempre valide o state

Sem validação do state, sua aplicação fica vulnerável a ataques CSRF:

python
# Correto if request.args.get("state") != session.get("oauth_state"): return "Erro: possível ataque CSRF", 400 # Errado — ignorar o state code = request.args.get("code") # Vulnerável!

3. Troque o código imediatamente

O código de autorização expira em 10 minutos e é de uso único. Troque-o por tokens assim que recebê-lo.

4. Armazene tokens com criptografia

Nunca salve tokens em texto puro no banco de dados. Use criptografia (AES, Fernet) e, idealmente, um gerenciador de segredos.

5. Solicite apenas escopos necessários

Quanto menos permissões você pedir, mais confiança o usuário terá na sua aplicação. Siga o princípio do menor privilégio.

6. Implemente renovação automática

Não espere o access token expirar para renovar. Renove com antecedência (ex: 5 minutos antes) para evitar falhas.

7. Trate a revogação

Se um usuário revogar o acesso pelo painel do Bunto, seus tokens param de funcionar imediatamente. Sua aplicação deve tratar o erro 401 e guiar o usuário a reconectar.


FAQ

P: Preciso de HTTPS na redirect_uri? R: Em produção, sim, é obrigatório. Em desenvolvimento local, HTTP é permitido para facilitar testes.

P: Posso ter mais de uma redirect_uri? R: Sim, até 5 por aplicativo. Na hora da autorização, a redirect_uri enviada deve corresponder exatamente a uma das cadastradas.

P: O que acontece quando o refresh token expira? R: O usuário precisa autorizar novamente. Sua aplicação deve detectar o erro e redirecionar o usuário para o fluxo de autorização.

P: Um usuário pode revogar o acesso? R: Sim, a qualquer momento em Integrações → Aplicativos no painel do Bunto ERP. Todos os tokens são invalidados imediatamente.

P: O access token funciona em todos os endpoints da API v1? R: Apenas nos módulos e ações autorizados pelos escopos. Tentativas de acessar módulos fora do escopo retornam 403.

P: O OAuth substitui o Token de API? R: Não. São complementares. Token de API é para integrações da própria empresa (mais simples). OAuth é para aplicações de terceiros que acessam dados de outros usuários (mais seguro).

P: Posso usar o oauthdebugger.com para testar? R: Sim! Configure a Authorize URI como https://erp.bunto.com.br/oauth/autorizar/ e a redirect URI como https://oauthdebugger.com/debug. Lembre-se de cadastrar essa URI no seu aplicativo OAuth.

P: Os tokens são armazenados em texto puro no Bunto? R: Não. Todos os tokens são criptografados com Fernet (criptografia simétrica) antes de serem armazenados. A validação usa comparação constant-time para prevenir timing attacks.

APIAutenticaçãoC#GoJavaJavaScriptOAuthPHPPythonREST
Recursos para IA