Skip to content

Commit

Permalink
Extension improvements
Browse files Browse the repository at this point in the history
- address comments by nicklaw5
- create Extension Claimsfunc  to accept struct as parameter
- improve JWT unit tests
- refactor 'SendExtensionPubSubMessage' structs to be prefixed by 'Extension'
  • Loading branch information
jackmcguire1 committed Aug 31, 2021
1 parent 5459ac2 commit 754614f
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 53 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,6 @@ ID: 23161357 Name: lirik
## Contributions

PRs are very much welcome.
All new features should rely solely on the Go standard library.
No external dependencies should be included in your solutions.
Where possible, please include tests for any code that is introduced by your PRs.

## License
Expand Down
2 changes: 1 addition & 1 deletion channels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func TestGetChannelInformation(t *testing.T) {

for i, channel := range resp.Data.Channels {
if channel.BroadcasterID != testCase.parsed[i].BroadcasterID {
t.Errorf("Expected struct field BroadcasterID = %s, was %s", testCase.parsed[i].BroadcasterID, channel.BroadcasterID)
t.Errorf("Expected struct field ChannelID = %s, was %s", testCase.parsed[i].BroadcasterID, channel.BroadcasterID)
}

if channel.BroadcasterName != testCase.parsed[i].BroadcasterName {
Expand Down
13 changes: 6 additions & 7 deletions docs/extensions_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,25 @@
## Extension Helix Requests

### Generate PUBSUB JWT Permissions
> relevant PUBSUB permission must be passed to the 'ExtensionCreateClaims()' func, in order to correctly publish a pubsub message of a particular type
Relevant PUBSUB permission must be passed to the 'ExtensionCreateClaims()' func, in order to correctly publish a pubsub message of a particular type.

Broadcast pubsub type
Broadcast pubsub type:
```go
client.FormBroadcastSendPubSubPermissions()
```

Global pubsub type
Global pubsub type:
```go
perms := client.FormGlobalSendPubSubPermissions()
```

Whisper User type
Whisper User type:
```go
client.FormWhisperSendPubSubPermissions(userId)
```

### JWT ROLES
> Note:- Currently only the 'external' role is supported by helix endpoints
Note: Currently only the 'external' role is supported by helix endpoints.

### EBS JWT
this is used to set the correct header for any Extension helix requests
Expand Down Expand Up @@ -77,7 +76,7 @@ if err != nil {
// handle error
}

// set the JWT token to be used as in the Auth bearer header
// Set the JWT token to be used as in the Auth bearer header.
jwt := client.ExtensionJWTSign(claims)
client.SetExtensionSignedJWTToken(jwt)

Expand Down
11 changes: 6 additions & 5 deletions extension_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ func (s ExtensionSegmentType) String() string {
}

type ExtensionSetConfigurationParams struct {
Segment ExtensionSegmentType `json:"segment"`
ExtensionID string `json:"extension-id"`
BroadcasterID string `json:"broadcaster_id,omitempty"` // populated if segment is of type 'developer' || 'broadcaster'
Version string `json:"version"`
Content string `json:"content"`
Segment ExtensionSegmentType `json:"segment"`
ExtensionID string `json:"extension-id"`
// BroadcasterID is only populated if segment is of type 'developer' || 'broadcaster'
BroadcasterID string `json:"broadcaster_id,omitempty"`
Version string `json:"version"`
Content string `json:"content"`
}

type ExtensionConfigurationSegment struct {
Expand Down
47 changes: 26 additions & 21 deletions extension_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import (
)

// RoleType The user role type
type roleType string
type RoleType string

// Types of user roles used within the JWT Claims
// TODO expose these when helix supports them
const (
BroadcasterRole roleType = "broadcaster"
ExternalRole roleType = "external"
ModeratorRole roleType = "moderator"
ViewerRole roleType = "viewer"
BroadcasterRole RoleType = "broadcaster"
ExternalRole RoleType = "external"
ModeratorRole RoleType = "moderator"
ViewerRole RoleType = "viewer"

// toAllChannels this user type roll is used for sending global pubsub messages
toAllChannels = "all"
)

Expand All @@ -35,21 +35,25 @@ type TwitchJWTClaims struct {
OpaqueUserID string `json:"opaque_user_id,omitempty"`
UserID string `json:"user_id"`
ChannelID string `json:"channel_id,omitempty"`
Role roleType `json:"role"`
Role RoleType `json:"role"`
Unlinked bool `json:"is_unlinked,omitempty"`
Permissions *PubSubPermissions `json:"pubsub_perms"`
jwt.StandardClaims
}

type ExtensionCreateClaimsParams struct {
// ChannelID if this value is empty it will default to 'all'
ChannelID string
// PubSub is the pubsub permission to attach to the claim
PubSub *PubSubPermissions
// Wxpiration is the epoch of jwt expiration, default 3 minutes from time.Now
Expiration int64
}

// CreateClaims will construct a claims suitable for generating a JWT token,
// containing necessary information required by the Twitch API.
// @param BroadcasterID if this value is empty it will default to 'all'
// @param pubsub the pubsub permission to attach to the claim
// @param expiration the epoch of jwt expiration, default 3 minutes from time.Now
// containing necessary information required by the Twitch Helix Extension API endpoints.
func (c *Client) ExtensionCreateClaims(
broadcasterID string,
pubsub *PubSubPermissions,
expiration int64,
params *ExtensionCreateClaimsParams,
) (
*TwitchJWTClaims,
error,
Expand All @@ -60,21 +64,22 @@ func (c *Client) ExtensionCreateClaims(
}

// default expiration to 3 minutes
if expiration == 0 {
expiration = time.Now().Add(time.Minute*3).UnixNano() / int64(time.Millisecond)
if params.Expiration == 0 {
params.Expiration = time.Now().Add(time.Minute*3).UnixNano() / int64(time.Millisecond)
}

if broadcasterID == "" {
broadcasterID = toAllChannels
// default channelID to 'all'
if params.ChannelID == "" {
params.ChannelID = toAllChannels
}

claims := &TwitchJWTClaims{
UserID: c.opts.ExtensionOpts.OwnerUserID,
ChannelID: broadcasterID,
ChannelID: params.ChannelID,
Role: ExternalRole,
Permissions: pubsub,
Permissions: params.PubSub,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expiration,
ExpiresAt: params.Expiration,
},
}

Expand Down
67 changes: 58 additions & 9 deletions extension_jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func TestValidateJwtParameters(t *testing.T) {
t.Parallel()
c := newMockClient(&Options{}, newMockHandler(http.StatusOK, "", nil))

_, err := c.ExtensionCreateClaims("", c.FormBroadcastSendPubSubPermissions(), 0)
err := c.validateExtensionOpts()
if err == nil {
t.Errorf("expected to get an error got nil")
}
Expand All @@ -23,7 +23,7 @@ func TestValidateJwtParameters(t *testing.T) {
ExtensionOpts: ExtensionOptions{OwnerUserID: "100249558"},
}, newMockHandler(http.StatusOK, "", nil))

_, err = c.ExtensionCreateClaims("", c.FormBroadcastSendPubSubPermissions(), 0)
err = c.validateExtensionOpts()
if err == nil {
t.Errorf("expected to get an error got nil")
}
Expand All @@ -43,19 +43,30 @@ func TestCreateClaims(t *testing.T) {
},
}, newMockHandler(http.StatusOK, "", nil))

claims, err := c.ExtensionCreateClaims("", c.FormBroadcastSendPubSubPermissions(), 0)
channelID := "1337"
params := &ExtensionCreateClaimsParams{
ChannelID: channelID,
PubSub: c.FormBroadcastSendPubSubPermissions(),
Expiration: 0,
}

claims, err := c.ExtensionCreateClaims(params)
if err != nil {
t.Errorf("unexpected error generating claims %s", err)
}
if claims.UserID != userId {
t.Errorf("claims userId doesn't match got %s expected %s", claims.UserID, userId)
}
if claims.ChannelID != channelID {
t.Errorf("claims broadcasterId doesn't match got %s expected %s", claims.ChannelID, channelID)
}
if claims.ExpiresAt < time.Now().Add(4*time.Minute).UnixNano() && claims.ExpiresAt > time.Now().Add(-2*time.Minute).UnixNano() {
t.Errorf("claims expiry less than 3 minutes")
}

expiration := time.Now().Add(10*time.Minute).UnixNano() / int64(time.Millisecond)
claims, err = c.ExtensionCreateClaims("100249558", c.FormBroadcastSendPubSubPermissions(), expiration)
params.Expiration = expiration
claims, err = c.ExtensionCreateClaims(params)
if err != nil {
t.Errorf("unexpected error generating claims %s", err)
}
Expand All @@ -77,7 +88,12 @@ func TestSignClaimsToJWT(t *testing.T) {
},
}, newMockHandler(http.StatusOK, "", nil))

claims, err := c.ExtensionCreateClaims("100249558", c.FormBroadcastSendPubSubPermissions(), 0)
params := &ExtensionCreateClaimsParams{
ChannelID: "1337",
PubSub: c.FormBroadcastSendPubSubPermissions(),
Expiration: 0,
}
claims, err := c.ExtensionCreateClaims(params)
if err != nil {
t.Errorf("unexpected error generating claims %s", err)
}
Expand All @@ -101,11 +117,18 @@ func TestVerifyJWT(t *testing.T) {
},
}, newMockHandler(http.StatusOK, "", nil))

broadcasterId := "1337"
claims, err := c.ExtensionCreateClaims(broadcasterId, c.FormBroadcastSendPubSubPermissions(), 0)
channelID := "1337"
params := &ExtensionCreateClaimsParams{
ChannelID: channelID,
PubSub: c.FormBroadcastSendPubSubPermissions(),
Expiration: 0,
}

claims, err := c.ExtensionCreateClaims(params)
if err != nil {
t.Errorf("unexpected error generating claims %s", err)
}

jwt, err := c.ExtensionJWTSign(claims)
if err != nil {
t.Errorf("failed to sign claims %s", err)
Expand All @@ -119,14 +142,40 @@ func TestVerifyJWT(t *testing.T) {
t.Errorf("unexpected error verifying JWT err:%s", err)
}

claims, err = c.ExtensionJWTVerify("abcd")
if err != nil && !strings.Contains(err.Error(), "token contains an invalid number of segments") {
t.Errorf("unexpected error verifying JWT err:%s", err)
}

claims, err = c.ExtensionJWTVerify(jwt)
if err != nil && !strings.Contains(err.Error(), "JWT token string missing") {
t.Errorf("unexpected error verifying JWT err:%s", err)
}
if claims.ChannelID != broadcasterId {
t.Errorf("found unexpected broadcaster in claims got:%s expected:%s", claims.ChannelID, broadcasterId)
if claims.ChannelID != channelID {
t.Errorf("found unexpected broadcaster in claims got:%s expected:%s", claims.ChannelID, channelID)
}
if claims.UserID != userId {
t.Errorf("found unexpected userId in claims got:%s expected:%s", claims.UserID, userId)
}

// generate expired claims to vefiry expiration behaviour
params.Expiration = time.Now().Add(-10 * time.Minute).Unix()

claims, err = c.ExtensionCreateClaims(params)
if err != nil {
t.Errorf("unexpected error generating claims %s", err)
}

jwt, err = c.ExtensionJWTSign(claims)
if err != nil {
t.Errorf("failed to sign claims %s", err)
}
if jwt == "" {
t.Errorf("JWT token is empty")
}

claims, err = c.ExtensionJWTVerify(jwt)
if err != nil && !strings.Contains(err.Error(), "token is expired by 10m") {
t.Errorf("unexpected error verifying JWT err:%s", err)
}
}
10 changes: 5 additions & 5 deletions extension_pubsub.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,24 @@ func (c *Client) FormGenericPubSubPermissions() *PubSubPermissions {
}
}

type SendExtensionPubSubMessageParams struct {
type ExtensionSendPubSubMessageParams struct {
BroadcasterID string `json:"broadcaster_id"`
Message string `json:"message"`
Target []ExtensionPubSubPublishType `json:"target"`
IsGlobalBroadcast bool `json:"is_global_broadcast"`
}

type SendExtensionPubSubMessageResponse struct {
type ExtensionSendPubSubMessageResponse struct {
ResponseCommon
}

func (c *Client) SendExtensionPubSubMessage(params *SendExtensionPubSubMessageParams) (*SendExtensionPubSubMessageResponse, error) {
resp, err := c.postAsJSON("/extensions/pubsub", &SendExtensionPubSubMessageResponse{}, params)
func (c *Client) SendExtensionPubSubMessage(params *ExtensionSendPubSubMessageParams) (*ExtensionSendPubSubMessageResponse, error) {
resp, err := c.postAsJSON("/extensions/pubsub", &ExtensionSendPubSubMessageResponse{}, params)
if err != nil {
return nil, err
}

sndExtPubSubMsgRsp := &SendExtensionPubSubMessageResponse{}
sndExtPubSubMsgRsp := &ExtensionSendPubSubMessageResponse{}
resp.HydrateResponseCommon(&sndExtPubSubMsgRsp.ResponseCommon)

return sndExtPubSubMsgRsp, nil
Expand Down
6 changes: 3 additions & 3 deletions extension_pubsub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ func TestExtensionSendPubSubMessage(t *testing.T) {
testCases := []struct {
statusCode int
options *Options
params *SendExtensionPubSubMessageParams
params *ExtensionSendPubSubMessageParams
respBody string
validationErr string
}{
{
http.StatusUnauthorized,
&Options{ClientID: "my-client-id"},
&SendExtensionPubSubMessageParams{},
&ExtensionSendPubSubMessageParams{},
`{"error":"Unauthorized","status":401,"message":"JWT token is missing"}`,
"",
},
Expand All @@ -98,7 +98,7 @@ func TestExtensionSendPubSubMessage(t *testing.T) {
OwnerUserID: "ext-owner-id",
},
},
&SendExtensionPubSubMessageParams{
&ExtensionSendPubSubMessageParams{
BroadcasterID: "100249558",
Message: "{}",
Target: []ExtensionPubSubPublishType{ExtensionPubSubBroadcastPublish},
Expand Down

0 comments on commit 754614f

Please sign in to comment.