cli to support token based auth

This commit is contained in:
David Zhao
2020-12-27 22:02:19 -08:00
parent 46dadc25c2
commit 43a6db8a3a
13 changed files with 170 additions and 39 deletions
+35 -3
View File
@@ -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
View File
@@ -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
}
+24
View File
@@ -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
View File
@@ -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{}
+18
View File
@@ -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) {
+10 -10
View File
@@ -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
View File
@@ -7,5 +7,6 @@ type VideoGrant struct {
}
type ClaimGrants struct {
Video *VideoGrant `json:"video,omitempty"`
Identity string `json:"-"`
Video *VideoGrant `json:"video,omitempty"`
}
+1 -1
View File
@@ -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
+5 -2
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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())