mirror of
https://github.com/livekit/livekit.git
synced 2026-07-28 18:39:41 +00:00
cli to support token based auth
This commit is contained in:
@@ -5,8 +5,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/twitchtv/twirp"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/auth"
|
||||
"github.com/livekit/livekit-server/pkg/logger"
|
||||
"github.com/livekit/livekit-server/proto/livekit"
|
||||
)
|
||||
|
||||
@@ -23,6 +26,8 @@ var (
|
||||
Usage: "name of the room",
|
||||
Required: true,
|
||||
},
|
||||
apiKeyFlag,
|
||||
secretFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -32,6 +37,8 @@ var (
|
||||
Flags: []cli.Flag{
|
||||
roomFlag,
|
||||
roomHostFlag,
|
||||
apiKeyFlag,
|
||||
secretFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -41,6 +48,8 @@ var (
|
||||
Flags: []cli.Flag{
|
||||
roomFlag,
|
||||
roomHostFlag,
|
||||
apiKeyFlag,
|
||||
secretFlag,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -55,7 +64,9 @@ func createClient(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func createRoom(c *cli.Context) error {
|
||||
room, err := roomClient.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
|
||||
ctx := contextWithAccessToken(c, &auth.VideoGrant{RoomCreate: true})
|
||||
fmt.Println("context", ctx)
|
||||
room, err := roomClient.CreateRoom(ctx, &livekit.CreateRoomRequest{
|
||||
Name: c.String("name"),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -67,8 +78,9 @@ func createRoom(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func getRoom(c *cli.Context) error {
|
||||
ctx := contextWithAccessToken(c, &auth.VideoGrant{RoomJoin: true})
|
||||
roomId := c.String("room")
|
||||
room, err := roomClient.GetRoom(context.Background(), &livekit.GetRoomRequest{
|
||||
room, err := roomClient.GetRoom(ctx, &livekit.GetRoomRequest{
|
||||
Room: roomId,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -80,8 +92,9 @@ func getRoom(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func deleteRoom(c *cli.Context) error {
|
||||
ctx := contextWithAccessToken(c, &auth.VideoGrant{RoomCreate: true})
|
||||
roomId := c.String("room")
|
||||
_, err := roomClient.DeleteRoom(context.Background(), &livekit.DeleteRoomRequest{
|
||||
_, err := roomClient.DeleteRoom(ctx, &livekit.DeleteRoomRequest{
|
||||
Room: roomId,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -91,3 +104,22 @@ func deleteRoom(c *cli.Context) error {
|
||||
fmt.Println("deleted room", roomId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func contextWithAccessToken(c *cli.Context, grant *auth.VideoGrant) context.Context {
|
||||
ctx := context.Background()
|
||||
token, err := accessToken(c, grant, "")
|
||||
if err != nil {
|
||||
logger.GetLogger().Errorw("Could not get access token", "err", err)
|
||||
}
|
||||
if token != "" {
|
||||
header := make(http.Header)
|
||||
header.Set("Authorization", "Bearer "+token)
|
||||
if tctx, err := twirp.WithHTTPRequestHeaders(ctx, header); err == nil {
|
||||
logger.GetLogger().Debugw("requesting with token")
|
||||
ctx = tctx
|
||||
} else {
|
||||
logger.GetLogger().Errorw("Error setting Twirp auth header", "err", err)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
+42
-10
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/livekit/livekit-server/cmd/cli/client"
|
||||
"github.com/livekit/livekit-server/pkg/auth"
|
||||
"github.com/livekit/livekit-server/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -29,12 +31,11 @@ var (
|
||||
rtcHostFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "access token, not required in dev mode",
|
||||
Usage: "access token, not required in dev mode. if passed in, ignores --api-key, --api-secret, and --name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "name of participant",
|
||||
Required: true,
|
||||
Name: "name",
|
||||
Usage: "name of participant",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "audio",
|
||||
@@ -44,6 +45,8 @@ var (
|
||||
Name: "video",
|
||||
Usage: "an ivf file to publish upon connection",
|
||||
},
|
||||
apiKeyFlag,
|
||||
secretFlag,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -55,16 +58,45 @@ func joinRoom(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("room_id", c.String("room-id"))
|
||||
v.Set("token", c.String("token"))
|
||||
v.Set("name", c.String("name"))
|
||||
u.RawQuery = v.Encode()
|
||||
name := c.String("name")
|
||||
roomId := c.String("room")
|
||||
token := c.String("token")
|
||||
|
||||
// generate access token if needed
|
||||
if token == "" {
|
||||
// require roomId & name to be passed in
|
||||
if roomId == "" {
|
||||
return fmt.Errorf("--room is required")
|
||||
}
|
||||
if name == "" {
|
||||
return fmt.Errorf("--name is required")
|
||||
}
|
||||
// token may be nil in dev mode
|
||||
token, err = accessToken(c, &auth.VideoGrant{
|
||||
RoomJoin: true,
|
||||
Room: roomId,
|
||||
}, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log := logger.GetLogger()
|
||||
var requestHeader http.Header
|
||||
if token != "" {
|
||||
// set this as a header
|
||||
requestHeader = make(http.Header)
|
||||
requestHeader.Set("Authorization", "Bearer "+token)
|
||||
} else {
|
||||
// dev mode, will pass these as overrides
|
||||
v := url.Values{}
|
||||
v.Set("room_id", roomId)
|
||||
v.Set("name", name)
|
||||
u.RawQuery = v.Encode()
|
||||
}
|
||||
|
||||
log.Infow("connecting to Websocket signal", "url", u.String())
|
||||
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(u.String(), requestHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,6 +26,14 @@ var (
|
||||
Name: "host",
|
||||
Value: "ws://localhost:7881",
|
||||
}
|
||||
apiKeyFlag = &cli.StringFlag{
|
||||
Name: "api-key",
|
||||
EnvVars: []string{"LK_API_KEY"},
|
||||
}
|
||||
secretFlag = &cli.StringFlag{
|
||||
Name: "api-secret",
|
||||
EnvVars: []string{"LK_API_SECRET"},
|
||||
}
|
||||
)
|
||||
|
||||
func PrintJSON(obj interface{}) {
|
||||
@@ -39,3 +49,17 @@ func ExpandUser(p string) string {
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func accessToken(c *cli.Context, grant *auth.VideoGrant, identity string) (value string, err error) {
|
||||
apiKey := c.String("api-key")
|
||||
apiSecret := c.String("api-secret")
|
||||
if apiKey == "" && apiSecret == "" {
|
||||
// not provided, don't sign request
|
||||
return
|
||||
}
|
||||
|
||||
at := auth.NewAccessToken(apiKey, apiSecret).
|
||||
AddGrant(grant).
|
||||
SetIdentity(identity)
|
||||
return at.ToJWT()
|
||||
}
|
||||
|
||||
+10
-1
@@ -7,6 +7,10 @@ import (
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultValidDuration = 10 * time.Minute
|
||||
)
|
||||
|
||||
// Signer that produces token signed with API key and secret
|
||||
type AccessToken struct {
|
||||
apiKey string
|
||||
@@ -49,10 +53,15 @@ func (t *AccessToken) ToJWT() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
validFor := defaultValidDuration
|
||||
if t.validFor > 0 {
|
||||
t.validFor = validFor
|
||||
}
|
||||
|
||||
cl := jwt.Claims{
|
||||
Issuer: t.apiKey,
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Expiry: jwt.NewNumericDate(time.Now().Add(t.validFor)),
|
||||
Expiry: jwt.NewNumericDate(time.Now().Add(validFor)),
|
||||
ID: t.identity,
|
||||
}
|
||||
grants := &ClaimGrants{}
|
||||
|
||||
@@ -42,6 +42,24 @@ func TestAPIIssuer(t *testing.T) {
|
||||
|
||||
assert.EqualValues(t, videoGrant, decodedGrant.Video)
|
||||
})
|
||||
|
||||
t.Run("default validity should be more than a minute", func(t *testing.T) {
|
||||
apiKey, secret := apiKeypair()
|
||||
videoGrant := &auth.VideoGrant{RoomJoin: true, Room: "myroom"}
|
||||
at := auth.NewAccessToken(apiKey, secret).
|
||||
AddGrant(videoGrant)
|
||||
value, err := at.ToJWT()
|
||||
token, err := jwt.ParseSigned(value)
|
||||
|
||||
claim := jwt.Claims{}
|
||||
decodedGrant := auth.ClaimGrants{}
|
||||
err = token.UnsafeClaimsWithoutVerification(&claim, &decodedGrant)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, videoGrant, decodedGrant.Video)
|
||||
|
||||
// default validity
|
||||
assert.True(t, claim.Expiry.Time().Sub(claim.IssuedAt.Time()) > time.Minute)
|
||||
})
|
||||
}
|
||||
|
||||
func apiKeypair() (string, string) {
|
||||
|
||||
@@ -18,17 +18,17 @@ type FakeTokenVerifier struct {
|
||||
identityReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
}
|
||||
VerifyStub func(interface{}) (*auth.VideoGrant, error)
|
||||
VerifyStub func(interface{}) (*auth.ClaimGrants, error)
|
||||
verifyMutex sync.RWMutex
|
||||
verifyArgsForCall []struct {
|
||||
arg1 interface{}
|
||||
}
|
||||
verifyReturns struct {
|
||||
result1 *auth.VideoGrant
|
||||
result1 *auth.ClaimGrants
|
||||
result2 error
|
||||
}
|
||||
verifyReturnsOnCall map[int]struct {
|
||||
result1 *auth.VideoGrant
|
||||
result1 *auth.ClaimGrants
|
||||
result2 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
@@ -88,7 +88,7 @@ func (fake *FakeTokenVerifier) IdentityReturnsOnCall(i int, result1 string) {
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeTokenVerifier) Verify(arg1 interface{}) (*auth.VideoGrant, error) {
|
||||
func (fake *FakeTokenVerifier) Verify(arg1 interface{}) (*auth.ClaimGrants, error) {
|
||||
fake.verifyMutex.Lock()
|
||||
ret, specificReturn := fake.verifyReturnsOnCall[len(fake.verifyArgsForCall)]
|
||||
fake.verifyArgsForCall = append(fake.verifyArgsForCall, struct {
|
||||
@@ -113,7 +113,7 @@ func (fake *FakeTokenVerifier) VerifyCallCount() int {
|
||||
return len(fake.verifyArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeTokenVerifier) VerifyCalls(stub func(interface{}) (*auth.VideoGrant, error)) {
|
||||
func (fake *FakeTokenVerifier) VerifyCalls(stub func(interface{}) (*auth.ClaimGrants, error)) {
|
||||
fake.verifyMutex.Lock()
|
||||
defer fake.verifyMutex.Unlock()
|
||||
fake.VerifyStub = stub
|
||||
@@ -126,28 +126,28 @@ func (fake *FakeTokenVerifier) VerifyArgsForCall(i int) interface{} {
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeTokenVerifier) VerifyReturns(result1 *auth.VideoGrant, result2 error) {
|
||||
func (fake *FakeTokenVerifier) VerifyReturns(result1 *auth.ClaimGrants, result2 error) {
|
||||
fake.verifyMutex.Lock()
|
||||
defer fake.verifyMutex.Unlock()
|
||||
fake.VerifyStub = nil
|
||||
fake.verifyReturns = struct {
|
||||
result1 *auth.VideoGrant
|
||||
result1 *auth.ClaimGrants
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeTokenVerifier) VerifyReturnsOnCall(i int, result1 *auth.VideoGrant, result2 error) {
|
||||
func (fake *FakeTokenVerifier) VerifyReturnsOnCall(i int, result1 *auth.ClaimGrants, result2 error) {
|
||||
fake.verifyMutex.Lock()
|
||||
defer fake.verifyMutex.Unlock()
|
||||
fake.VerifyStub = nil
|
||||
if fake.verifyReturnsOnCall == nil {
|
||||
fake.verifyReturnsOnCall = make(map[int]struct {
|
||||
result1 *auth.VideoGrant
|
||||
result1 *auth.ClaimGrants
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.verifyReturnsOnCall[i] = struct {
|
||||
result1 *auth.VideoGrant
|
||||
result1 *auth.ClaimGrants
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,5 +7,6 @@ type VideoGrant struct {
|
||||
}
|
||||
|
||||
type ClaimGrants struct {
|
||||
Video *VideoGrant `json:"video,omitempty"`
|
||||
Identity string `json:"-"`
|
||||
Video *VideoGrant `json:"video,omitempty"`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ var (
|
||||
//counterfeiter:generate . TokenVerifier
|
||||
type TokenVerifier interface {
|
||||
Identity() string
|
||||
Verify(key interface{}) (*VideoGrant, error)
|
||||
Verify(key interface{}) (*ClaimGrants, error)
|
||||
}
|
||||
|
||||
//counterfeiter:generate . KeyProvider
|
||||
|
||||
@@ -39,7 +39,7 @@ func (v *APIKeyTokenVerifier) Identity() string {
|
||||
return v.identity
|
||||
}
|
||||
|
||||
func (v *APIKeyTokenVerifier) Verify(key interface{}) (*VideoGrant, error) {
|
||||
func (v *APIKeyTokenVerifier) Verify(key interface{}) (*ClaimGrants, error) {
|
||||
if key == nil || key == "" {
|
||||
return nil, ErrKeysMissing
|
||||
}
|
||||
@@ -54,5 +54,8 @@ func (v *APIKeyTokenVerifier) Verify(key interface{}) (*VideoGrant, error) {
|
||||
if err := out.Validate(jwt.Expected{Issuer: v.apiKey, Time: time.Now()}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims.Video, nil
|
||||
|
||||
// copy over identity
|
||||
claims.Identity = out.ID
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,6 @@ func TestAPIVerifier(t *testing.T) {
|
||||
|
||||
decoded, err := v.Verify(secret)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &claim, decoded)
|
||||
assert.Equal(t, &claim, decoded.Video)
|
||||
})
|
||||
}
|
||||
|
||||
+6
-6
@@ -73,8 +73,8 @@ func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func GetGrants(ctx context.Context) *auth.VideoGrant {
|
||||
claims, ok := ctx.Value(grantsKey).(*auth.VideoGrant)
|
||||
func GetGrants(ctx context.Context) *auth.ClaimGrants {
|
||||
claims, ok := ctx.Value(grantsKey).(*auth.ClaimGrants)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -90,13 +90,13 @@ func EnsureJoinPermission(ctx context.Context) (name string, err error) {
|
||||
return "", nil
|
||||
}
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil {
|
||||
if claims == nil || claims.Video == nil {
|
||||
err = ErrPermissionDenied
|
||||
return
|
||||
}
|
||||
|
||||
if claims.RoomJoin {
|
||||
name = claims.Room
|
||||
if claims.Video.RoomJoin {
|
||||
name = claims.Video.Room
|
||||
} else {
|
||||
err = ErrPermissionDenied
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func EnsureCreatePermission(ctx context.Context) error {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
|
||||
if claims.RoomCreate {
|
||||
if claims.Video.RoomCreate {
|
||||
return nil
|
||||
}
|
||||
return ErrPermissionDenied
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestAuthMiddleware(t *testing.T) {
|
||||
provider.GetSecretReturns(secret)
|
||||
|
||||
m := service.NewAPIKeyAuthMiddleware(provider)
|
||||
var grants *auth.VideoGrant
|
||||
var grants *auth.ClaimGrants
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
grants = service.GetGrants(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -38,7 +38,7 @@ func TestAuthMiddleware(t *testing.T) {
|
||||
m.ServeHTTP(w, r, handler)
|
||||
|
||||
assert.NotNil(t, grants)
|
||||
assert.EqualValues(t, orig, grants)
|
||||
assert.EqualValues(t, orig, grants.Video)
|
||||
|
||||
// no authorization == no claims
|
||||
grants = nil
|
||||
|
||||
+14
-2
@@ -17,15 +17,17 @@ import (
|
||||
type RTCService struct {
|
||||
manager *rtc.RoomManager
|
||||
upgrader websocket.Upgrader
|
||||
isDev bool
|
||||
}
|
||||
|
||||
func NewRTCService(conf *config.Config, manager *rtc.RoomManager) *RTCService {
|
||||
s := &RTCService{
|
||||
manager: manager,
|
||||
upgrader: websocket.Upgrader{},
|
||||
isDev: conf.Development,
|
||||
}
|
||||
|
||||
if conf.Development {
|
||||
if s.isDev {
|
||||
s.upgrader.CheckOrigin = func(r *http.Request) bool {
|
||||
// allow all in dev
|
||||
return true
|
||||
@@ -37,7 +39,17 @@ func NewRTCService(conf *config.Config, manager *rtc.RoomManager) *RTCService {
|
||||
|
||||
func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
roomId := r.FormValue("room_id")
|
||||
pName := r.FormValue("name")
|
||||
var pName string
|
||||
if s.isDev {
|
||||
r.FormValue("name")
|
||||
} else {
|
||||
claims := GetGrants(r.Context())
|
||||
// require a claim
|
||||
if claims == nil || claims.Video == nil {
|
||||
writeJSONError(w, http.StatusUnauthorized, rtc.ErrPermissionDenied.Error())
|
||||
}
|
||||
pName = claims.Identity
|
||||
}
|
||||
log := logger.GetLogger()
|
||||
|
||||
onlyName, err := EnsureJoinPermission(r.Context())
|
||||
|
||||
Reference in New Issue
Block a user