Obter e reaproveitar o token
O token vale 30 minutos e não tem refresh token. O cliente abaixo guarda o token em memória, pede um novo 60 s antes de vencer e descarta o guardado ao receber 401. As regras estão em Acesso.
As credenciais vêm de variáveis de ambiente ou de um cofre de segredos, nunca do código-fonte.
- C#
- Node.js
- Python
- Java
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public sealed class EtechTokenProvider(HttpClient http, string apiKey, string clientId, string clientSecret)
{
static readonly TimeSpan Margem = TimeSpan.FromSeconds(60);
readonly SemaphoreSlim _trava = new(1, 1);
string? _token;
DateTimeOffset _venceEm;
public async Task<string> ObterAsync(CancellationToken ct = default)
{
if (_token is not null && DateTimeOffset.UtcNow < _venceEm - Margem)
return _token;
await _trava.WaitAsync(ct);
try
{
if (_token is not null && DateTimeOffset.UtcNow < _venceEm - Margem)
return _token;
using var pedido = new HttpRequestMessage(HttpMethod.Post, "/oauth2/token")
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = clientId,
["client_secret"] = clientSecret,
}),
};
pedido.Headers.Add("x-api-key", apiKey);
using var resposta = await http.SendAsync(pedido, ct);
resposta.EnsureSuccessStatusCode();
var corpo = await resposta.Content.ReadFromJsonAsync<RespostaToken>(ct)
?? throw new InvalidOperationException("Resposta de token vazia.");
_token = corpo.AccessToken;
_venceEm = DateTimeOffset.UtcNow.AddSeconds(corpo.ExpiresIn);
return _token;
}
finally
{
_trava.Release();
}
}
/// Chame ao receber 401 numa rota de integração, antes de repetir a chamada uma vez.
public void Descartar() => _token = null;
sealed record RespostaToken(
[property: JsonPropertyName("access_token")] string AccessToken,
[property: JsonPropertyName("expires_in")] int ExpiresIn);
}
O HttpClient do .NET não manda User-Agent por padrão. Configure-o uma vez:
http.BaseAddress = new Uri("https://vm-integration.staging.etechsa.com.br");
http.DefaultRequestHeaders.UserAgent.ParseAdd("MeuErp/1.0");
const BASE_URL = 'https://vm-integration.staging.etechsa.com.br';
const USER_AGENT = 'MeuErp/1.0';
const MARGEM_MS = 60_000;
let token = null;
let venceEm = 0;
let pedidoEmAndamento = null;
export async function obterToken() {
if (token && Date.now() < venceEm - MARGEM_MS) return token;
pedidoEmAndamento ??= pedirToken().finally(() => { pedidoEmAndamento = null; });
return pedidoEmAndamento;
}
/** Chame ao receber 401 numa rota de integração, antes de repetir a chamada uma vez. */
export function descartarToken() {
token = null;
}
async function pedirToken() {
const resposta = await fetch(`${BASE_URL}/oauth2/token`, {
method: 'POST',
headers: {
'x-api-key': process.env.ETECH_API_KEY,
'User-Agent': USER_AGENT,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.ETECH_CLIENT_ID,
client_secret: process.env.ETECH_CLIENT_SECRET,
}),
});
if (!resposta.ok) throw new Error(`Token recusado: ${resposta.status}`);
const corpo = await resposta.json();
token = corpo.access_token;
venceEm = Date.now() + corpo.expires_in * 1000;
return token;
}
import os
import threading
import time
import requests
BASE_URL = "https://vm-integration.staging.etechsa.com.br"
USER_AGENT = "MeuErp/1.0"
MARGEM_SEGUNDOS = 60
_trava = threading.Lock()
_token: str | None = None
_vence_em = 0.0
def obter_token() -> str:
global _token, _vence_em
with _trava:
if _token and time.time() < _vence_em - MARGEM_SEGUNDOS:
return _token
resposta = requests.post(
f"{BASE_URL}/oauth2/token",
headers={"x-api-key": os.environ["ETECH_API_KEY"], "User-Agent": USER_AGENT},
data={
"grant_type": "client_credentials",
"client_id": os.environ["ETECH_CLIENT_ID"],
"client_secret": os.environ["ETECH_CLIENT_SECRET"],
},
timeout=10,
)
resposta.raise_for_status()
corpo = resposta.json()
_token = corpo["access_token"]
_vence_em = time.time() + corpo["expires_in"]
return _token
def descartar_token() -> None:
"""Chame ao receber 401 numa rota de integração, antes de repetir a chamada uma vez."""
global _token
with _trava:
_token = None
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public final class EtechTokenProvider {
private static final String BASE_URL = "https://vm-integration.staging.etechsa.com.br";
private static final String USER_AGENT = "MeuErp/1.0";
private static final Duration MARGEM = Duration.ofSeconds(60);
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper json = new ObjectMapper();
private String token;
private Instant venceEm = Instant.EPOCH;
public synchronized String obter() throws Exception {
if (token != null && Instant.now().isBefore(venceEm.minus(MARGEM))) return token;
String form = "grant_type=client_credentials"
+ "&client_id=" + enc(System.getenv("ETECH_CLIENT_ID"))
+ "&client_secret=" + enc(System.getenv("ETECH_CLIENT_SECRET"));
HttpRequest pedido = HttpRequest.newBuilder(URI.create(BASE_URL + "/oauth2/token"))
.header("x-api-key", System.getenv("ETECH_API_KEY"))
.header("User-Agent", USER_AGENT)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
HttpResponse<String> resposta = http.send(pedido, HttpResponse.BodyHandlers.ofString());
if (resposta.statusCode() != 200) throw new IllegalStateException("Token recusado: " + resposta.statusCode());
JsonNode corpo = json.readTree(resposta.body());
token = corpo.get("access_token").asText();
venceEm = Instant.now().plusSeconds(corpo.get("expires_in").asLong());
return token;
}
/** Chame ao receber 401 numa rota de integração, antes de repetir a chamada uma vez. */
public synchronized void descartar() {
token = null;
}
private static String enc(String valor) {
return URLEncoder.encode(valor, StandardCharsets.UTF_8);
}
}