Skip to content

Commit

Permalink
webhook support
Browse files Browse the repository at this point in the history
  • Loading branch information
davidzhao committed Aug 9, 2021
1 parent 9caa97d commit 992c4b2
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 45 deletions.
55 changes: 39 additions & 16 deletions auth/accesstoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ const (

// Signer that produces token signed with API key and secret
type AccessToken struct {
apiKey string
secret string
identity string
videoGrant *VideoGrant
metadata string
validFor time.Duration
apiKey string
secret string
grant ClaimGrants
validFor time.Duration
}

func NewAccessToken(key string, secret string) *AccessToken {
Expand All @@ -29,7 +27,7 @@ func NewAccessToken(key string, secret string) *AccessToken {
}

func (t *AccessToken) SetIdentity(identity string) *AccessToken {
t.identity = identity
t.grant.Identity = identity
return t
}

Expand All @@ -39,12 +37,17 @@ func (t *AccessToken) SetValidFor(duration time.Duration) *AccessToken {
}

func (t *AccessToken) AddGrant(grant *VideoGrant) *AccessToken {
t.videoGrant = grant
t.grant.Video = grant
return t
}

func (t *AccessToken) SetMetadata(md string) *AccessToken {
t.metadata = md
t.grant.Metadata = md
return t
}

func (t *AccessToken) SetSha256(sha string) *AccessToken {
t.grant.Sha256 = sha
return t
}

Expand All @@ -68,14 +71,34 @@ func (t *AccessToken) ToJWT() (string, error) {
Issuer: t.apiKey,
NotBefore: jwt.NewNumericDate(time.Now()),
Expiry: jwt.NewNumericDate(time.Now().Add(validFor)),
ID: t.identity,
Subject: t.grant.Identity,
// eventually deprecate using ID as identity
ID: t.grant.Identity,
}
grants := &ClaimGrants{}
if t.videoGrant != nil {
grants.Video = t.videoGrant
return jwt.Signed(sig).Claims(cl).Claims(&t.grant).CompactSerialize()
}

func (t *AccessToken) toJWTOld() (string, error) {
if t.apiKey == "" || t.secret == "" {
return "", ErrKeysMissing
}
if t.metadata != "" {
grants.Metadata = t.metadata

sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: []byte(t.secret)},
(&jose.SignerOptions{}).WithType("JWT"))
if err != nil {
return "", err
}

validFor := defaultValidDuration
if t.validFor > 0 {
validFor = t.validFor
}

cl := jwt.Claims{
Issuer: t.apiKey,
NotBefore: jwt.NewNumericDate(time.Now()),
Expiry: jwt.NewNumericDate(time.Now().Add(validFor)),
ID: t.grant.Identity,
}
return jwt.Signed(sig).Claims(cl).Claims(grants).CompactSerialize()
return jwt.Signed(sig).Claims(cl).Claims(&t.grant).CompactSerialize()
}
58 changes: 38 additions & 20 deletions auth/accesstoken_test.go
Original file line number Diff line number Diff line change
@@ -1,66 +1,84 @@
package auth_test
package auth

import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2/jwt"

"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/utils"
)

func TestAccessToken(t *testing.T) {
t.Parallel()

t.Run("keys must be set", func(t *testing.T) {
token := auth.NewAccessToken("", "")
token := NewAccessToken("", "")
_, err := token.ToJWT()
assert.Equal(t, auth.ErrKeysMissing, err)
require.Equal(t, ErrKeysMissing, err)
})

t.Run("generates a decodeable key", func(t *testing.T) {
t.Run("generates a decode-able key", func(t *testing.T) {
apiKey, secret := apiKeypair()
videoGrant := &auth.VideoGrant{RoomJoin: true, Room: "myroom"}
at := auth.NewAccessToken(apiKey, secret).
videoGrant := &VideoGrant{RoomJoin: true, Room: "myroom"}
at := NewAccessToken(apiKey, secret).
AddGrant(videoGrant).
SetValidFor(time.Minute * 5).
SetIdentity("user")
value, err := at.ToJWT()
//fmt.Println(raw)
assert.NoError(t, err)
require.NoError(t, err)

assert.Len(t, strings.Split(value, "."), 3)
require.Len(t, strings.Split(value, "."), 3)

// ensure it's a valid JWT
token, err := jwt.ParseSigned(value)
assert.NoError(t, err)
require.NoError(t, err)

decodedGrant := auth.ClaimGrants{}
decodedGrant := ClaimGrants{}
err = token.UnsafeClaimsWithoutVerification(&decodedGrant)
assert.NoError(t, err)
require.NoError(t, err)

assert.EqualValues(t, videoGrant, decodedGrant.Video)
require.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).
videoGrant := &VideoGrant{RoomJoin: true, Room: "myroom"}
at := NewAccessToken(apiKey, secret).
AddGrant(videoGrant)
value, err := at.ToJWT()
token, err := jwt.ParseSigned(value)

claim := jwt.Claims{}
decodedGrant := auth.ClaimGrants{}
decodedGrant := ClaimGrants{}
err = token.UnsafeClaimsWithoutVerification(&claim, &decodedGrant)
assert.NoError(t, err)
assert.EqualValues(t, videoGrant, decodedGrant.Video)
require.NoError(t, err)
require.EqualValues(t, videoGrant, decodedGrant.Video)

// default validity
assert.True(t, claim.Expiry.Time().Sub(claim.IssuedAt.Time()) > time.Minute)
require.True(t, claim.Expiry.Time().Sub(claim.IssuedAt.Time()) > time.Minute)
})

t.Run("backwards compatible with jti identity tokens", func(t *testing.T) {
apiKey, secret := apiKeypair()
videoGrant := &VideoGrant{RoomJoin: true, Room: "myroom"}
at := NewAccessToken(apiKey, secret).
AddGrant(videoGrant).
SetValidFor(time.Minute * 5).
SetIdentity("user")
value, err := at.toJWTOld()
//fmt.Println(raw)
require.NoError(t, err)

verifier, err := ParseAPIToken(value)
require.NoError(t, err)

grants, err := verifier.Verify(secret)
require.NoError(t, err)
require.Equal(t, "user", grants.Identity)
})
}

Expand Down
4 changes: 3 additions & 1 deletion auth/grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ type VideoGrant struct {
type ClaimGrants struct {
Identity string `json:"-"`
Video *VideoGrant `json:"video,omitempty"`
Metadata string `json:"metadata,omitempty"`
// for verifying integrity of the message body
Sha256 string `json:"sha256,omitempty"`
Metadata string `json:"metadata,omitempty"`
}
14 changes: 9 additions & 5 deletions auth/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ func ParseAPIToken(raw string) (*APIKeyTokenVerifier, error) {
return nil, err
}

return &APIKeyTokenVerifier{
v := &APIKeyTokenVerifier{
token: tok,
apiKey: out.Issuer,
identity: out.ID,
}, nil
identity: out.Subject,
}
if v.identity == "" {
v.identity = out.ID
}
return v, nil
}

// Returns the API key this token was signed with
// APIKey returns the API key this token was signed with
func (v *APIKeyTokenVerifier) APIKey() string {
return v.apiKey
}
Expand All @@ -57,6 +61,6 @@ func (v *APIKeyTokenVerifier) Verify(key interface{}) (*ClaimGrants, error) {
}

// copy over identity
claims.Identity = out.ID
claims.Identity = v.identity
return &claims, nil
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ module github.com/livekit/protocol
go 1.16

require (
github.com/google/go-cmp v0.5.5 // indirect
github.com/go-logr/logr v1.0.0
github.com/jxskiss/base62 v0.0.0-20191017122030-4f11678b909b
github.com/lithammer/shortuuid/v3 v3.0.6
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b // indirect
google.golang.org/protobuf v1.27.1
gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-logr/logr v1.0.0 h1:kH951GinvFVaQgy/ki/B3YYmQtRpExGigSJg6O8z5jo=
github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
Expand Down Expand Up @@ -85,6 +88,9 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
1 change: 0 additions & 1 deletion livekit_recording.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "livekit_models.proto";
// Recording service that can be performed on any node
// they are Twirp-based HTTP req/responses
service RecordingService {

// Starts a room
rpc StartRecording(StartRecordingRequest) returns (RecordingResponse);

Expand Down
1 change: 0 additions & 1 deletion livekit_room.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "livekit_models.proto";
// Room service that can be performed on any node
// they are Twirp-based HTTP req/responses
service RoomService {

// Creates a room with settings. Requires `roomCreate` permission.
// This method is optional; rooms are automatically created when clients connect to them for the first time.
rpc CreateRoom(CreateRoomRequest) returns (Room);
Expand Down
27 changes: 27 additions & 0 deletions livekit_webhook.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
syntax = "proto3";

package livekit;
option go_package = "github.com/livekit/livekit-server/proto/livekit";

import "livekit_models.proto";

message WebhookEvent {
// one of room_started, room_ended, participant_joined, participant_left, recording_started, recording_finished
string type = 1;

// JWT token signed with the first API key pair
string token = 2;

Room room = 3;

// set when event is participant_*
ParticipantInfo participant = 4;

// set when event is recording_*
string recording_id = 5;
// oneof recording_output {
// string file = 7;
// RecordingS3Output s3 = 8;
// string rtmp = 9;
// }
}
76 changes: 76 additions & 0 deletions webhook/notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package webhook

import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"time"

"github.com/go-logr/logr"
"github.com/livekit/protocol/auth"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)

const authHeader = "Authorization"

type Notifier struct {
apiKey string
apiSecret string
urls []string
Logger logr.Logger
}

func NewNotifier(apiKey, apiSecret string, urls []string) *Notifier {
return &Notifier{
apiKey: apiKey,
apiSecret: apiSecret,
urls: urls,
Logger: logr.Discard(),
}
}

func (n *Notifier) Notify(payload interface{}) error {
var encoded []byte
var err error
if message, ok := payload.(proto.Message); ok {
// use proto marshaler to ensure lowerCaseCamel
encoded, err = protojson.Marshal(message)
} else {
// encode as JSON
encoded, err = json.Marshal(payload)
}
if err != nil {
return err
}

// sign payload
sum := sha256.Sum256(encoded)
b64 := base64.StdEncoding.EncodeToString(sum[:])

at := auth.NewAccessToken(n.apiKey, n.apiSecret).
SetValidFor(5 * time.Minute).
SetSha256(b64)
token, err := at.ToJWT()
if err != nil {
return err
}

for _, url := range n.urls {
r, err := http.NewRequest("POST", url, bytes.NewReader(encoded))
if err != nil {
// ignore and continue
n.Logger.Error(err, "could not create request", "url", url)
continue
}
r.Header.Set(authHeader, token)
_, err = http.DefaultClient.Do(r)
if err != nil {
n.Logger.Error(err, "could not post to webhook", "url", url)
}
}

return nil
}
Loading

0 comments on commit 992c4b2

Please sign in to comment.