package rtc import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "strings" "time" ) type TokenClaims struct { Version int `json:"v"` RoomID string `json:"room_id"` UserID string `json:"user_id"` ExpiresAt int64 `json:"exp"` } type TokenIssuer interface { Issue(roomID, userID string, expiresAt time.Time) (string, error) } type LocalTokenIssuer struct{ secret []byte } func NewLocalTokenIssuer(secret []byte) (*LocalTokenIssuer, error) { if len(secret) < 32 { return nil, errors.New("RTC token signing secret must contain at least 32 bytes") } return &LocalTokenIssuer{secret: append([]byte(nil), secret...)}, nil } func (i *LocalTokenIssuer) Issue(roomID, userID string, expiresAt time.Time) (string, error) { if roomID == "" || userID == "" || expiresAt.IsZero() { return "", errors.New("room, user and expiry are required") } payload, err := json.Marshal(TokenClaims{Version: 1, RoomID: roomID, UserID: userID, ExpiresAt: expiresAt.Unix()}) if err != nil { return "", err } encoded := base64.RawURLEncoding.EncodeToString(payload) return "dev1." + encoded + "." + base64.RawURLEncoding.EncodeToString(i.sign(encoded)), nil } func (i *LocalTokenIssuer) Verify(token string, now time.Time) (TokenClaims, error) { parts := strings.Split(token, ".") if len(parts) != 3 || parts[0] != "dev1" { return TokenClaims{}, errors.New("invalid RTC token") } signature, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil || !hmac.Equal(signature, i.sign(parts[1])) { return TokenClaims{}, errors.New("invalid RTC token") } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return TokenClaims{}, errors.New("invalid RTC token") } var claims TokenClaims if err := json.Unmarshal(payload, &claims); err != nil || claims.Version != 1 || claims.RoomID == "" || claims.UserID == "" { return TokenClaims{}, errors.New("invalid RTC token") } if now.Unix() >= claims.ExpiresAt { return TokenClaims{}, errors.New("RTC token expired") } return claims, nil } func (i *LocalTokenIssuer) sign(payload string) []byte { mac := hmac.New(sha256.New, i.secret) _, _ = mac.Write([]byte(payload)) return mac.Sum(nil) }