Skip to content

คีย์ชั่วคราว

ห้ามเปิดเผย secret_key ที่มีอายุยาวนานในโค้ดฝั่งเบราว์เซอร์

สำหรับการผสานรวมบนเว็บ เบราว์เซอร์ควรได้รับเฉพาะคีย์ชั่วคราวที่มีอายุสั้น ในทางปฏิบัติ พาร์ทเนอร์จำนวนมากจะสร้าง endpoint ขนาดเล็กบนเซิร์ฟเวอร์ เช่น POST /api/ephemeral-key ซึ่ง endpoint นี้จะเรียก SORI Console API จาก ฝั่งเซิร์ฟเวอร์

เหตุผลที่ควรใช้คีย์ชั่วคราว

ในการผสานรวมบนเว็บ ผู้ใช้สามารถเข้าถึงโค้ดฝั่งเบราว์เซอร์ได้ หากใส่ secret_key ที่มีอายุยาวนานไว้ในโค้ดดังกล่าว คีย์อาจถูกดึงออกไปและนำไปใช้ ในทางที่ผิดนอกบริการของคุณ

คีย์ชั่วคราวช่วยป้องกันปัญหานี้ เซิร์ฟเวอร์ของคุณจะเก็บ secret_key ที่มีอายุยาวนานไว้เป็นความลับ แลกคีย์ดังกล่าวเป็นคีย์ชั่วคราวที่มีอายุสั้น และส่งเฉพาะคีย์ชั่วคราวนั้นกลับไปยังเบราว์เซอร์ จากนั้นเบราว์เซอร์สามารถใช้ คีย์ชั่วคราวเพื่อยืนยันตัวตนและเรียก SDK API ต่อไปนี้ โดยไม่เปิดเผยคีย์ลับเดิม

วิธีใช้งานฝั่งไคลเอนต์ที่แนะนำ

โดยทั่วไป AudioRecognizer ควรรับคีย์ผ่าน ephemeralKey

1. ดึงคีย์ภายใน 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. ดึงคีย์ก่อน แล้วส่งให้โดยตรง

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();

การสร้าง endpoint สำหรับแลกคีย์ของคุณเอง

หากคุณดูแล endpoint ของตนเอง หน้าที่ของ endpoint มีดังนี้:

  1. รับคำขอจากเบราว์เซอร์ เช่น POST /api/ephemeral-key
  2. อ่าน app_id และ secret_key จากการกำหนดค่าฝั่งเซิร์ฟเวอร์
  3. เรียก endpoint สำหรับคีย์ชั่วคราวของ SORI Console API บนเซิร์ฟเวอร์
  4. ส่ง payload การตอบกลับที่มีอายุสั้นนั้นกลับไปยังเบราว์เซอร์

เมื่อใช้ base URL เริ่มต้นของ SORI API endpoint ต้นทางคือ:

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

เนื้อหาคำขอต้นทางเข้ารหัสแบบฟอร์ม:

txt
app_id=YOUR_APP_ID
secret_key=YOUR_SECRET_KEY

endpoint ของคุณต้องไม่ส่งคืน secret_key ที่มีอายุยาวนาน

การตอบกลับของ SORI Console API

ใน sori_console เมื่อ POST /api/auth/ephemeral สำเร็จ จะส่งคืนข้อมูลในรูปแบบต่อไปนี้:

json
{
  "success": true,
  "ephemeral_key": "eyJhbGciOi...",
  "ephemeral_key_expires_at": "2026-03-23T12:34:56Z"
}
  • success: ผลลัพธ์ของคำขอ
  • ephemeral_key: คีย์ที่มีอายุสั้นสำหรับ SDK ฝั่งเบราว์เซอร์
  • ephemeral_key_expires_at: เวลาหมดอายุในรูปแบบ ISO 8601

หน้าที่ของเซิร์ฟเวอร์

  • เก็บ app_id และ secret_key ไว้ในตัวแปรสภาพแวดล้อมฝั่งเซิร์ฟเวอร์หรือพื้นที่จัดเก็บข้อมูลลับ
  • เรียก SORI Console API ด้วยข้อมูลที่เข้ารหัสแบบฟอร์ม
  • หากการตอบกลับจากต้นทางไม่สำเร็จ ให้ส่งคืนสถานะดังกล่าวและหยุดการทำงาน
  • ส่งเฉพาะ payload การตอบกลับที่มีอายุสั้นกลับไปยังเบราว์เซอร์

ตัวอย่างการนำไปใช้

ตัวอย่างต่อไปนี้ใช้ขั้นตอนการแลกคีย์เดียวกันทั้งหมด:

  • Node.js: POST /api/ephemeral-key
  • Python: POST /api/ephemeral-key
  • Java: POST /api/ephemeral-key
  • PHP: POST /api/sori_ephemeral_key.php
  • การเรียกต้นทาง: POST https://console.soriapi.com/api/auth/ephemeral
  • การตอบกลับไปยังเบราว์เซอร์: รูปแบบ JSON เดียวกับ SORI Console API
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;

หมายเหตุ

  • เบราว์เซอร์ควรเรียกเฉพาะ endpoint สำหรับคีย์ที่มีอายุสั้นของคุณ ไม่ใช่เส้นทางจัดการคีย์ลับที่มีอายุยาวนาน
  • หากคุณโฮสต์แอป SORI หลายแอป ตัวส่งต่อสามารถเลือกค่า app_id ที่แตกต่างกันตามคำขอ เทนเนนต์ หรือสภาพแวดล้อมการนำไปใช้งาน
  • หากต้องใช้ middleware หรือการยืนยันตัวตนเฉพาะเฟรมเวิร์ก ให้สัญญาการเชื่อมต่อกับเบราว์เซอร์เรียบง่ายและเสถียร