Clave efímera
Nunca exponga la secret_key de larga duración en el código del navegador.
En las integraciones Web, el navegador solo debe recibir una clave efímera de corta duración. En la práctica, muchos partners implementan un pequeño endpoint de servidor, como POST /api/ephemeral-key, que llama a la API de SORI Console desde el servidor.
Por qué utilizar una clave efímera
En una integración Web, el código del navegador queda expuesto al usuario. Si incluye en él su secret_key de larga duración, esta puede extraerse y utilizarse de forma indebida fuera de su servicio.
Una clave efímera evita este problema. Su servidor mantiene en privado la secret_key de larga duración, la intercambia por una clave efímera de corta duración y devuelve al navegador únicamente esa clave temporal. De este modo, el navegador puede utilizarla para autenticarse y realizar las siguientes llamadas a la API del SDK sin exponer el secreto original.
Uso recomendado en el cliente
Por lo general, AudioRecognizer debe recibir la clave mediante ephemeralKey.
1. Obtener la clave dentro de ephemeralKey
import { AudioRecognizer } from "@sorisdk/web-audio";
const recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: async () => {
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
return ephemeral_key;
}
});
recognizer.on("campaign", (event) => {
console.log(event.campaign);
});
await recognizer.start();2. Obtener primero la clave y pasarla directamente
import { AudioRecognizer } from "@sorisdk/web-audio";
const response = await fetch("/api/ephemeral-key", {
method: "POST"
});
if (!response.ok) {
throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
}
const { ephemeral_key } = await response.json();
const recognizer = new AudioRecognizer({
appId: "YOUR_APP_ID",
ephemeralKey: ephemeral_key
});
recognizer.on("campaign", (event) => {
console.log(event.campaign);
});
await recognizer.start();Implementar su propio endpoint de intercambio de claves
Si gestiona su propio endpoint, su función es sencilla:
- Recibir una solicitud del navegador, como
POST /api/ephemeral-key. - Leer
app_idysecret_keyde la configuración del servidor. - Llamar al endpoint de claves efímeras de la API de SORI Console desde el servidor.
- Devolver al navegador el payload de respuesta de corta duración.
Con la URL base predeterminada de la API de SORI, el endpoint upstream es:
https://console.soriapi.com/api/auth/ephemeralEl cuerpo de la solicitud upstream está codificado como formulario:
app_id=YOUR_APP_ID
secret_key=YOUR_SECRET_KEYSu endpoint nunca debe devolver la secret_key de larga duración.
Respuesta de la API de SORI Console
En sori_console, POST /api/auth/ephemeral devuelve la siguiente estructura si la solicitud se completa correctamente:
{
"success": true,
"ephemeral_key": "eyJhbGciOi...",
"ephemeral_key_expires_at": "2026-03-23T12:34:56Z"
}success: resultado de la solicitudephemeral_key: clave de corta duración para el SDK del navegadorephemeral_key_expires_at: fecha y hora de vencimiento en formato ISO 8601
Responsabilidades del servidor
- Mantener
app_idysecret_keyen variables de entorno del servidor o en un almacén de secretos. - Llamar a la API de SORI Console con datos codificados como formulario.
- Si la respuesta upstream no es correcta, devolver su estado y detener el proceso.
- Devolver al navegador únicamente el payload de respuesta de corta duración.
Ejemplos de implementación
Todos los ejemplos siguientes implementan el mismo flujo de intercambio de claves:
- Node.js:
POST /api/ephemeral-key - Python:
POST /api/ephemeral-key - Java:
POST /api/ephemeral-key - PHP:
POST /api/sori_ephemeral_key.php - llamada upstream:
POST https://console.soriapi.com/api/auth/ephemeral - respuesta del navegador: la misma estructura JSON que la API de SORI Console
import express from "express";
const app = express();
const APP_ID = process.env.SORI_APP_ID;
const SECRET_KEY = process.env.SORI_SECRET_KEY;
const SORI_EPHEMERAL_ENDPOINT = "https://console.soriapi.com/api/auth/ephemeral";
app.post("/api/ephemeral-key", async (_req, res) => {
if (!APP_ID || !SECRET_KEY) {
res.status(500).json({ detail: "Missing SORI_APP_ID or SORI_SECRET_KEY" });
return;
}
try {
const upstream = await fetch(SORI_EPHEMERAL_ENDPOINT, {
method: "POST",
body: new URLSearchParams({
app_id: APP_ID,
secret_key: SECRET_KEY
})
});
const raw = await upstream.text();
if (!upstream.ok) {
res.status(upstream.status).type("application/json").send(raw);
return;
}
res.type("application/json").send(raw);
} catch (error) {
res.status(500).json({
detail: error instanceof Error ? error.message : String(error)
});
}
});
app.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});import os
import requests
from fastapi import FastAPI
from fastapi.responses import JSONResponse, Response
app = FastAPI()
APP_ID = os.getenv("SORI_APP_ID")
SECRET_KEY = os.getenv("SORI_SECRET_KEY")
SORI_EPHEMERAL_ENDPOINT = "https://console.soriapi.com/api/auth/ephemeral"
@app.post("/api/ephemeral-key")
def create_sori_ephemeral_key():
if not APP_ID or not SECRET_KEY:
return JSONResponse(
status_code=500,
content={"detail": "Missing SORI_APP_ID or SORI_SECRET_KEY"},
)
try:
upstream = requests.post(
SORI_EPHEMERAL_ENDPOINT,
data={
"app_id": APP_ID,
"secret_key": SECRET_KEY,
},
timeout=10,
)
except requests.RequestException as exc:
return JSONResponse(status_code=500, content={"detail": str(exc)})
if not upstream.ok:
return Response(
content=upstream.text,
status_code=upstream.status_code,
media_type="application/json",
)
return Response(content=upstream.text, media_type="application/json")package example.sori;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class SoriEphemeralKeyController {
private final OkHttpClient httpClient = new OkHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
private final String appId = System.getenv("SORI_APP_ID");
private final String secretKey = System.getenv("SORI_SECRET_KEY");
private static final String SORI_EPHEMERAL_ENDPOINT =
"https://console.soriapi.com/api/auth/ephemeral";
@PostMapping(value = "/api/ephemeral-key", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> createEphemeralKey() {
if (isBlank(appId) || isBlank(secretKey)) {
return json(500, Map.of(
"detail", "Missing SORI_APP_ID or SORI_SECRET_KEY"
));
}
RequestBody body = new FormBody.Builder()
.add("app_id", appId)
.add("secret_key", secretKey)
.build();
Request request = new Request.Builder()
.url(SORI_EPHEMERAL_ENDPOINT)
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
String rawBody = response.body() != null ? response.body().string() : "{}";
if (!response.isSuccessful()) {
return ResponseEntity.status(response.code())
.contentType(MediaType.APPLICATION_JSON)
.body(rawBody);
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(rawBody);
} catch (Exception error) {
return json(500, Map.of("detail", error.getMessage()));
}
}
private ResponseEntity<String> json(int status, Map<String, String> body) {
try {
return ResponseEntity.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(objectMapper.writeValueAsString(body));
} catch (Exception error) {
return ResponseEntity.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body("{\"detail\":\"Unexpected server error\"}");
}
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}<?php
// /api/sori_ephemeral_key.php
header('Content-Type: application/json');
$appId = getenv('SORI_APP_ID');
$secretKey = getenv('SORI_SECRET_KEY');
$soriEphemeralEndpoint = 'https://console.soriapi.com/api/auth/ephemeral';
if (!$appId || !$secretKey) {
http_response_code(500);
echo json_encode(['detail' => 'Missing SORI_APP_ID or SORI_SECRET_KEY']);
exit;
}
$curl = curl_init($soriEphemeralEndpoint);
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'app_id' => $appId,
'secret_key' => $secretKey,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$raw = curl_exec($curl);
$statusCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($raw === false) {
http_response_code(500);
echo json_encode(['detail' => curl_error($curl)]);
curl_close($curl);
exit;
}
curl_close($curl);
if ($statusCode < 200 || $statusCode >= 300) {
http_response_code($statusCode);
echo $raw;
exit;
}
echo $raw;Notas
- El navegador solo debe llamar a su endpoint de claves de corta duración, no a la ruta que gestiona el secreto de larga duración.
- Si aloja varias aplicaciones SORI, el relay puede seleccionar distintos valores de
app_idsegún la solicitud, el tenant o el entorno de despliegue. - Si necesita middleware o autenticación específicos de un framework, mantenga el contrato con el navegador sencillo y estable.
