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.
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ário | Mé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 conectar | OAuth 2.0 — cada usuário autoriza individualmente |
| SaaS que atende múltiplas empresas do Bunto | OAuth 2.0 — cada empresa autoriza o acesso |
| Plugin ou extensão para o ecossistema Bunto | OAuth 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:
- Sua aplicação redireciona o usuário para a tela de consentimento do Bunto
- O usuário vê quais permissões sua aplicação está pedindo e decide autorizar ou negar
- O Bunto redireciona de volta para sua aplicação com um código temporário
- Sua aplicação troca o código por tokens de acesso (server-to-server, seguro)
- 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
- Faça login no painel do Bunto ERP
- No menu lateral, acesse Integrações
- Clique na aba Aplicativos OAuth
- Clique no botão Novo Aplicativo
O que preencher
| Campo | Descrição | Exemplo |
|---|---|---|
| Nome | Nome exibido na tela de consentimento | "Minha Loja Virtual" |
| Descrição | O que sua aplicação faz (o usuário verá isso) | "Sincroniza pedidos e estoque com sua loja" |
| Redirect URIs | URLs de callback da sua aplicação (HTTPS obrigatório em produção) | https://meuapp.com/callback |
| Escopos | Permissões máximas que o app pode solicitar | produtos:read, vendas:read, vendas:write |
Credenciais geradas
Ao salvar, você receberá duas credenciais:
codeclient_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:
codeprodutos:read → Ler produtos vendas:read → Ler vendas vendas:write → Criar e atualizar vendas clientes:delete → Excluir clientes
Ações disponíveis:
| Ação | Métodos HTTP | O que permite |
|---|---|---|
read | GET, HEAD, OPTIONS | Consultar e listar dados |
write | POST, PUT, PATCH | Criar e atualizar registros |
delete | DELETE | Excluir 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:
codehttps://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âmetro | Descrição |
|---|---|
client_id | ID público do seu aplicativo (começa com bnt_app_) |
redirect_uri | URL de callback — deve ser exatamente igual à cadastrada no aplicativo |
scope | Escopos solicitados, separados por vírgula |
state | String aleatória para proteção contra CSRF — sempre gere um valor único |
response_type | Deve 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)
pythonimport 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)
javascriptconst 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)
csharpusing 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âmetro | Descrição |
|---|---|
code | Código de autorização temporário (uso único, expira em 10 minutos) |
state | O 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:
| Erro | Descrição |
|---|---|
invalid_request | Parâmetros faltando ou inválidos |
unauthorized_client | Aplicativo desativado |
invalid_scope | Escopos inválidos ou não permitidos para o app |
unsupported_response_type | Apenas code é suportado |
access_denied | O 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
codePOST https://api-backend.bunto.com.br/integracoes/oauth/token/ Content-Type: application/x-www-form-urlencoded
Parâmetros
| Parâmetro | Valor |
|---|---|
grant_type | authorization_code |
code | O código recebido no callback |
client_id | Seu bnt_app_xxx |
client_secret | Seu bnt_secret_xxx |
redirect_uri | A 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" }
| Campo | Descrição |
|---|---|
access_token | Token para acessar a API (prefixo bnt_oat_, válido por 4 horas) |
refresh_token | Token para renovar o access token (prefixo bnt_ort_, válido por 30 dias) |
token_type | Sempre Bearer |
expires_in | Segundos até expirar (14400 = 4 horas) |
scope | Escopos efetivamente concedidos |
Exemplos de código
cURL
bashcurl -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
pythonimport 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)
javascriptconst 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)
javaimport 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
gopackage 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:
bashcurl 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
pythonimport 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
javascriptconst 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
codePOST https://api-backend.bunto.com.br/integracoes/oauth/token/ Content-Type: application/x-www-form-urlencoded
Parâmetros
| Parâmetro | Valor |
|---|---|
grant_type | refresh_token |
refresh_token | Seu bnt_ort_xxx |
client_id | Seu bnt_app_xxx |
client_secret | Seu 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
pythonimport 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)
javascriptclass 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
codePOST https://api-backend.bunto.com.br/integracoes/oauth/revoke/ Content-Type: application/x-www-form-urlencoded
Parâmetros
| Parâmetro | Valor |
|---|---|
token | O token a ser revogado (access ou refresh) |
client_id | Seu bnt_app_xxx |
client_secret | Seu bnt_secret_xxx |
token_type_hint | access_token ou refresh_token (opcional) |
Exemplo
bashcurl -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
| Token | Prefixo | Validade | Armazenamento |
|---|---|---|---|
| Código de autorização | bnt_code_ | 10 minutos, uso único | Não armazene — troque imediatamente |
| Access token | bnt_oat_ | 4 horas | Banco de dados criptografado |
| Refresh token | bnt_ort_ | 30 dias | Banco 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" }
| Erro | Status | Causa | Solução |
|---|---|---|---|
invalid_client | 401 | client_id ou client_secret incorreto | Verifique as credenciais |
invalid_grant | 400 | Código expirado, já usado, ou redirect_uri diferente | Reinicie o fluxo OAuth |
unsupported_grant_type | 400 | grant_type inválido | Use authorization_code ou refresh_token |
Erros na API v1
| Status | Causa | Exemplo de resposta |
|---|---|---|
| 401 | Token expirado ou inválido | {"success": false, "message": "Token inválido"} |
| 403 | Escopo insuficiente | {"success": false, "message": "Permissão negada", "errors": {"detail": ["Token não tem permissão para este recurso"]}} |
| 403 | Módulo fora do plano | {"success": false, "message": "Permissão negada", "errors": {"detail": ["Módulo \"vendas\" não disponível no plano da empresa"]}} |
| 429 | Rate limit excedido | Aguarde e tente novamente com backoff |
Limites
| Recurso | Limite |
|---|---|
| Aplicativos por empresa | 5 |
| Redirect URIs por aplicativo | 5 |
| Código de autorização | 10 minutos, uso único |
| Access token | 4 horas |
| Refresh token | 30 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.