Obtención de Access Tokens
DVS utiliza el grant_type OAuth2 client credentials para autenticación service-to-service. El OSIGU Auth Server usa HTTP Basic auth para identificar al cliente y query string para el grant_type — esta página muestra la forma exacta de la request e implementaciones listas para copiar y pegar en 5 lenguajes.
Endpoints
| Entorno | Token URL |
|---|---|
| Sandbox | https://sandbox.osigu.com/v1/oauth/token |
| Producción | https://api.osigu.com/v1/oauth/token |
Las credenciales están limitadas por entorno — el client_id de sandbox no funciona contra la token URL de producción.
Forma de la request
grant_typestringrequiredEnviado como parámetro de query string (no en el body). Siempre client_credentials.
AuthorizationheaderrequiredHTTP Basic auth: Basic <base64(client_id:client_secret)>. Usar las credenciales emitidas por OSIGU para el entorno correspondiente.
Method: POST
Body: vacío
Content-Type: no requerido (sin body)
Response
{
"access_token": "7dd4f350-676e-4257-9d7b-f3c5ac4dfi14",
"token_type": "bearer",
"expires_in": 86399,
"scope": "read write",
"extensions": {
"provider_slug": "br-gamma"
}
}
| Campo | Descripción |
|---|---|
access_token | Token opaco (no un JWT). Enviarlo como Authorization: Bearer <token> en las llamadas subsecuentes a la API de DVS. |
token_type | Siempre bearer. |
expires_in | Segundos hasta la expiración. Por defecto 86399 (~24 horas). |
scope | Scope OAuth2 grueso otorgado al token (p. ej., read write). Las authorities más finas por endpoint de DVS se aplican del lado del servidor a partir de la configuración del client_id. |
extensions | Mapa de metadatos de forma libre. Incluye el provider_slug (usarlo para registrar a qué tenant pertenece el token). Puede incluir otros campos por tenant que OSIGU agregue en el futuro. |
Ejemplos de código
- curl
- Python
- Node.js
- PHP
- Java
curl --location --request POST \
"https://sandbox.osigu.com/v1/oauth/token?grant_type=client_credentials" \
--header "Authorization: Basic $(echo -n "$DVS_CLIENT_ID:$DVS_CLIENT_SECRET" | base64)"
import base64
import os
import time
import httpx
from threading import Lock
TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token" # cambiar a api.osigu.com en producción
CLIENT_ID = os.environ["DVS_CLIENT_ID"]
CLIENT_SECRET = os.environ["DVS_CLIENT_SECRET"]
_cache = {"access_token": None, "expires_at": 0.0, "provider_slug": None}
_lock = Lock()
def get_access_token() -> str:
"""Devuelve un access token válido, refrescándolo 60s antes de la expiración."""
with _lock:
if _cache["access_token"] and time.time() < _cache["expires_at"] - 60:
return _cache["access_token"]
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
response = httpx.post(
TOKEN_URL,
params={"grant_type": "client_credentials"},
headers={"Authorization": f"Basic {basic}"},
timeout=10,
)
response.raise_for_status()
payload = response.json()
_cache["access_token"] = payload["access_token"]
_cache["expires_at"] = time.time() + payload["expires_in"]
_cache["provider_slug"] = payload.get("extensions", {}).get("provider_slug")
return _cache["access_token"]
const TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token"; // cambiar a api.osigu.com en producción
let cached = { token: null, expiresAt: 0, providerSlug: null };
async function getAccessToken() {
if (cached.token && Date.now() < cached.expiresAt - 60_000) {
return cached.token;
}
const basic = Buffer.from(
`${process.env.DVS_CLIENT_ID}:${process.env.DVS_CLIENT_SECRET}`,
).toString("base64");
const response = await fetch(
`${TOKEN_URL}?grant_type=client_credentials`,
{
method: "POST",
headers: { Authorization: `Basic ${basic}` },
},
);
if (!response.ok) {
throw new Error(`Auth failed: ${response.status}`);
}
const payload = await response.json();
cached = {
token: payload.access_token,
expiresAt: Date.now() + payload.expires_in * 1000,
providerSlug: payload.extensions?.provider_slug ?? null,
};
return cached.token;
}
<?php
const TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token"; // cambiar a api.osigu.com en producción
function getAccessToken(): string {
static $cache = ["token" => null, "expires_at" => 0, "provider_slug" => null];
if ($cache["token"] && time() < $cache["expires_at"] - 60) {
return $cache["token"];
}
$basic = base64_encode(getenv("DVS_CLIENT_ID") . ":" . getenv("DVS_CLIENT_SECRET"));
$ch = curl_init(TOKEN_URL . "?grant_type=client_credentials");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Basic $basic"]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Auth failed: HTTP $status");
}
$payload = json_decode($body, true);
$cache["token"] = $payload["access_token"];
$cache["expires_at"] = time() + $payload["expires_in"];
$cache["provider_slug"] = $payload["extensions"]["provider_slug"] ?? null;
return $cache["token"];
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import org.json.JSONObject;
public class DvsAuth {
private static final String TOKEN_URL = "https://sandbox.osigu.com/v1/oauth/token"; // cambiar en producción
private static final HttpClient HTTP = HttpClient.newHttpClient();
private static volatile String cachedToken;
private static volatile Instant cachedExpiresAt = Instant.EPOCH;
private static volatile String cachedProviderSlug;
public static synchronized String getAccessToken() throws Exception {
if (cachedToken != null && Instant.now().isBefore(cachedExpiresAt.minusSeconds(60))) {
return cachedToken;
}
String creds = System.getenv("DVS_CLIENT_ID") + ":" + System.getenv("DVS_CLIENT_SECRET");
String basic = Base64.getEncoder().encodeToString(creds.getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(TOKEN_URL + "?grant_type=client_credentials"))
.header("Authorization", "Basic " + basic)
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Auth failed: " + response.statusCode());
}
JSONObject json = new JSONObject(response.body());
cachedToken = json.getString("access_token");
cachedExpiresAt = Instant.now().plusSeconds(json.getInt("expires_in"));
if (json.has("extensions")) {
JSONObject ext = json.getJSONObject("extensions");
cachedProviderSlug = ext.optString("provider_slug", null);
}
return cachedToken;
}
}
Buenas prácticas
Cachear los tokens
Los tokens son válidos por ~24 horas por defecto. Cachearlos en memoria (o en un caché compartido como Redis para servicios multi-instancia) y refrescarlos ~60 segundos antes de la expiración. Llamar al endpoint OAuth en cada request a la API resultará en rate limiting y latencia innecesaria.
Almacenar los secrets en un vault, no en archivos env
Usar AWS Secrets Manager, HashiCorp Vault, Doppler o equivalente. Rotar el client_secret anualmente (o tras cualquier sospecha de exposición) contactando a OSIGU.
Manejar 401 refrescando
Si una request devuelve 401, el token puede haber expirado o haber sido revocado. Descartar el token cacheado, solicitar uno nuevo y reintentar la request original una sola vez. No reintentar indefinidamente.
No loguear tokens
Eliminar los headers Authorization de los logs de la aplicación. Un token filtrado puede ser válido hasta 24 horas.
Usar extensions.provider_slug para observabilidad
Loguear el provider_slug de la response del token junto con los IDs de request. Esto acelera mucho la correlación de problemas con el soporte de OSIGU.
Errores
| Status | Body | Causa |
|---|---|---|
400 | error: "invalid_request" | Falta el grant_type en el query string. |
401 | error: "invalid_client" | client_id o client_secret incorrectos, o entorno equivocado (credenciales de sandbox contra la URL de producción). |
405 | Method Not Allowed | Se usó GET en lugar de POST. |
429 | rate limited | Demasiadas solicitudes de token. Cachear y reducir el ritmo. |
5xx | server error | Caída del Auth Server. Reintentar con exponential backoff (máx. 3). |