Skip to content

Ephemeral Key

長期 secret_key をブラウザコードに公開してはいけません。

Web連携では、ブラウザには短期的なephemeral keyだけを渡すべきです。実際には 多くのパートナーが POST /api/ephemeral-key のような小さなサーバーendpointを 用意し、そのendpointがサーバー側でSORI Console APIを呼び出す形で実装します。

なぜ Ephemeral Key を使うのか

Web連携では、ブラウザコードはユーザーから見える状態になります。そのコードに 長期 secret_key を入れてしまうと、サービス外部で取り出されて悪用される 可能性があります。

ephemeral key はその問題を避けるための仕組みです。サーバーは長期 secret_key を安全に保持したまま、短期の ephemeral key を発行し、その一時 キーだけをブラウザに返します。これにより、ブラウザは元の secret を公開せず に認証と後続の SDK API 呼び出しにその ephemeral key を使えます。

推奨されるクライアント利用方法

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

独自Key Exchange Endpointの実装

独自endpointを運用する場合、役割はシンプルです。

  1. ブラウザから POST /api/ephemeral-key のようなリクエストを受け取る
  2. サーバー設定から app_idsecret_key を読む
  3. サーバー側で SORI Console API のephemeral endpointを呼ぶ
  4. その短期レスポンスpayloadをブラウザへ返す

既定のSORI API baseを使う場合、upstream endpointは次のとおりです。

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

upstreamリクエスト本文は form-encoded です。

txt
app_id=YOUR_APP_ID
secret_key=YOUR_SECRET_KEY

独自endpointから長期 secret_key を返してはいけません。

SORI Console API の応答形式

sori_consolePOST /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_idsecret_key は環境変数またはsecret storageに保管する
  • SORI Console API は form-encoded データで呼び出す
  • upstreamレスポンスが成功でない場合は、そのステータスコードを返して終了する
  • ブラウザには短期レスポンスpayloadだけを返す

実装例

以下の例はすべて同じkey exchangeフローを実装します。

  • Node.js: POST /api/ephemeral-key
  • Python: POST /api/ephemeral-key
  • Java: POST /api/ephemeral-key
  • PHP: POST /api/sori_ephemeral_key.php
  • upstream call: POST https://console.soriapi.com/api/auth/ephemeral
  • browser response: SORI Console API と同じ JSON shape
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;

注意点

  • ブラウザは長期secret管理用の経路ではなく、短期キーを返すendpointだけを呼ぶようにしてください。
  • 複数のSORIアプリを運用している場合は、リクエスト、tenant、デプロイ環境に応じて異なる app_id を選ぶようrelayを拡張できます。
  • フレームワーク固有のmiddlewareや認証を追加しても、ブラウザ向け契約はシンプルで安定した形に保つのが望ましいです。