diff --git a/cmd/cli/commands/room.go b/cmd/cli/commands/room.go index 184fd4fc3..bb282d6de 100644 --- a/cmd/cli/commands/room.go +++ b/cmd/cli/commands/room.go @@ -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 +} diff --git a/cmd/cli/commands/rtc.go b/cmd/cli/commands/rtc.go index c14321278..795cbad50 100644 --- a/cmd/cli/commands/rtc.go +++ b/cmd/cli/commands/rtc.go @@ -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 } diff --git a/cmd/cli/commands/utils.go b/cmd/cli/commands/utils.go index 86b6cde65..60d5f93da 100644 --- a/cmd/cli/commands/utils.go +++ b/cmd/cli/commands/utils.go @@ -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() +} diff --git a/pkg/auth/accesstoken.go b/pkg/auth/accesstoken.go index f528bc9e3..888418810 100644 --- a/pkg/auth/accesstoken.go +++ b/pkg/auth/accesstoken.go @@ -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{} diff --git a/pkg/auth/accesstoken_test.go b/pkg/auth/accesstoken_test.go index e06ac4f20..f242efac1 100644 --- a/pkg/auth/accesstoken_test.go +++ b/pkg/auth/accesstoken_test.go @@ -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) { diff --git a/pkg/auth/authfakes/fake_token_verifier.go b/pkg/auth/authfakes/fake_token_verifier.go index a3cd92ece..4ffc42b94 100644 --- a/pkg/auth/authfakes/fake_token_verifier.go +++ b/pkg/auth/authfakes/fake_token_verifier.go @@ -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} } diff --git a/pkg/auth/grants.go b/pkg/auth/grants.go index 5e52e8320..c4ad5a3b2 100644 --- a/pkg/auth/grants.go +++ b/pkg/auth/grants.go @@ -7,5 +7,6 @@ type VideoGrant struct { } type ClaimGrants struct { - Video *VideoGrant `json:"video,omitempty"` + Identity string `json:"-"` + Video *VideoGrant `json:"video,omitempty"` } diff --git a/pkg/auth/interfaces.go b/pkg/auth/interfaces.go index a2821481e..187ac9505 100644 --- a/pkg/auth/interfaces.go +++ b/pkg/auth/interfaces.go @@ -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 diff --git a/pkg/auth/verifier.go b/pkg/auth/verifier.go index 17337ed6c..265f00411 100644 --- a/pkg/auth/verifier.go +++ b/pkg/auth/verifier.go @@ -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 } diff --git a/pkg/auth/verifier_test.go b/pkg/auth/verifier_test.go index a8afaa3b7..ca0d3caca 100644 --- a/pkg/auth/verifier_test.go +++ b/pkg/auth/verifier_test.go @@ -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) }) } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 2e3fe3d98..42a982a19 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -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 diff --git a/pkg/service/auth_test.go b/pkg/service/auth_test.go index 40f79d3f5..e0db9c882 100644 --- a/pkg/service/auth_test.go +++ b/pkg/service/auth_test.go @@ -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 diff --git a/pkg/service/rtc.go b/pkg/service/rtc.go index 3b2d3f266..08d0ab803 100644 --- a/pkg/service/rtc.go +++ b/pkg/service/rtc.go @@ -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())