Skip to content

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

ts
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

ts
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:

  1. Recibir una solicitud del navegador, como POST /api/ephemeral-key.
  2. Leer app_id y secret_key de la configuración del servidor.
  3. Llamar al endpoint de claves efímeras de la API de SORI Console desde el servidor.
  4. 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:

txt
https://console.soriapi.com/api/auth/ephemeral

El cuerpo de la solicitud upstream está codificado como formulario:

txt
app_id=YOUR_APP_ID
secret_key=YOUR_SECRET_KEY

Su 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:

json
{
  "success": true,
  "ephemeral_key": "eyJhbGciOi...",
  "ephemeral_key_expires_at": "2026-03-23T12:34:56Z"
}
  • success: resultado de la solicitud
  • ephemeral_key: clave de corta duración para el SDK del navegador
  • ephemeral_key_expires_at: fecha y hora de vencimiento en formato ISO 8601

Responsabilidades del servidor

  • Mantener app_id y secret_key en 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
js
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");
});
python
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")
java
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
<?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_id segú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.