DEV

Go で LiveKit の入室トークンを発行する最小サーバー

LiveKit の room に入るためのアクセストークンを Go で発行する最小構成。docker で dev server を立てて、発行 → claims 確認 → 署名検証まで実際にやったメモ。

LiveKit を触り始めた。クライアント(ブラウザ)が room に入るには、サーバー側で発行したアクセストークンが要る。これを Go で作る最小構成を組んで、docker で立てた dev server 相手に動かすところまでやった。

結論

  • LiveKit の入室許可は JWT(HMAC-SHA256 署名)
  • 発行に大きな SDK は要らない。github.com/livekit/protocol/auth パッケージだけで作れる
  • トークンには「どの room に」「誰が」「何をしていいか(grant)」を詰める
  • サーバーは受け取った JWT を API secret で署名検証して、通れば入室させる

環境(実測):Go 1.26.4 / github.com/livekit/protocol v1.49.0 / LiveKit server v1.13.2。

LiveKit の dev server を docker で立てる

--dev を付けると、鍵を用意しなくても固定の開発用キー(devkey / secret)で起動する。

Terminal
docker run -d --rm --name lk-dev -p 7880:7880 \
livekit/livekit-server --dev --bind 0.0.0.0

起動ログにキーが出る:

no keys provided, using placeholder keys {"API Key": "devkey", "API Secret": "secret"}
starting LiveKit server {"portHttp": 7880, ... "version": "1.13.2"}

この devkey / secret を、これからトークン発行サーバー側にも渡す。両者で同じ鍵を共有していることが署名検証の前提になる。

トークン発行サーバー (Go)

/token?room=...&identity=... を叩くと JWT を返すだけの HTTP サーバー。

main.go
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/livekit/protocol/auth"
)
// /token?room=<name>&identity=<user>
func getToken(w http.ResponseWriter, r *http.Request) {
room := r.URL.Query().Get("room")
identity := r.URL.Query().Get("identity")
if room == "" || identity == "" {
http.Error(w, "room and identity are required", http.StatusBadRequest)
return
}
apiKey := os.Getenv("LIVEKIT_API_KEY")
apiSecret := os.Getenv("LIVEKIT_API_SECRET")
at := auth.NewAccessToken(apiKey, apiSecret)
grant := &auth.VideoGrant{
RoomJoin: true,
Room: room,
}
at.SetVideoGrant(grant).
SetIdentity(identity).
SetValidFor(time.Hour)
token, err := at.ToJWT()
if err != nil {
http.Error(w, "failed to create token", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"token":"` + token + `"}`))
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/token", getToken)
log.Printf("token server listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}

やっていることは3つだけ:

  • auth.NewAccessToken(key, secret) — 鍵を渡してトークンビルダーを作る
  • VideoGrant{ RoomJoin: true, Room: room } — 「この room に入っていい」という許可
  • SetIdentity / SetValidFor — 誰の・何時間有効か

identity は room 内でその参加者を一意に識別する名前。同じ identity で2重に入ると後勝ちで前の接続が切られる、という挙動だった。

セットアップと起動

Terminal
go mod init lk-token
go get github.com/livekit/protocol/auth
go build -o lk-token .
# dev server と同じ鍵を渡す
LIVEKIT_API_KEY=devkey LIVEKIT_API_SECRET=secret PORT=8099 ./lk-token

叩く:

Terminal
curl "http://localhost:8099/token?room=demo&identity=alice"
# {"token":"eyJhbGci..."} (268文字くらいの JWT が返る)

中身を確認する

返ってきた JWT の真ん中(payload)を base64url でデコードすると、詰めた grant がそのまま入っている:

{
"iss": "devkey",
"sub": "alice",
"exp": 1782964258,
"nbf": 1782960658,
"iat": 1782960658,
"identity": "alice",
"video": { "roomJoin": true, "room": "demo" }
}
  • iss(発行者)に API key がそのまま入る。サーバーはこれを見て「どの secret で検証するか」を選ぶ
  • video が grant。roomJoin: true と room: "demo" がクライアントの権限
  • exp は SetValidFor(time.Hour) の1時間後

サーバーが「受理する」とはどういうことか

LiveKit サーバーが入室を許すかどうかは、JWT の HMAC-SHA256 署名が API secret(secret)と一致するかで決まる。同じ判定を手元で再現してみた:

Terminal (PowerShell)
# header.payload を secret で HMAC-SHA256 して、3つ目の署名部分と一致するか
$parts = $token.Split('.')
$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [Text.Encoding]::UTF8.GetBytes("secret")
$sig = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($parts[0] + "." + $parts[1]))
$b64 = [Convert]::ToBase64String($sig).Replace('+','-').Replace('/','_').TrimEnd('=')
$b64 -eq $parts[2] # → True

True が返った。つまり secret を知っている側なら誰でも同じ署名を再現できて、サーバーはそれで正当性を判断している。逆に言うと API secret が漏れると任意の grant のトークンを偽造できるので、secret はサーバー側だけに置く。クライアントに配るのは発行済みのトークンだけ。

詰まったところ

  • 発行サーバーの LIVEKIT_API_KEY / SECRET を dev server の devkey / secret と揃えないと、トークンは発行できるがサーバー側の署名検証で弾かれる。両者が同じ鍵を共有していることが大前提
  • -p 7880:7880 だけだと signaling(HTTP/WS)は通るが、実際のメディアには RTC 用のポート(7881/TCP・7882/UDP)も要る。トークン発行と入室確認だけなら 7880 で足りた

この先

トークンを配れれば、あとはブラウザ側の LiveKit クライアント SDK に serverUrl とこのトークンを渡せば room に入れる。次は Go 側から webhook(room 開始・参加者の出入り)を受ける口を作ってみる。