Skip to content

Commit

Permalink
feat: add jwt token signing and verification logic (#116)
Browse files Browse the repository at this point in the history
* feat: add jwt token signing and verification logic
  • Loading branch information
shwetha-manvinkurke authored Sep 13, 2021
1 parent ac8119c commit ca601d2
Show file tree
Hide file tree
Showing 21 changed files with 1,756 additions and 4 deletions.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,67 @@ func main() {
}
```

## Building Access Tokens
This library supports [access token](https://www.twilio.com/docs/iam/access-tokens) generation for use in the Twilio Client SDKs.

Here's how you would generate a token for the Voice SDK:
```go
package main

import
(
"os"
"github.com/twilio/twilio-go/client/jwt"
)

accountSid := os.Getenv("TWILIO_ACCOUNT_SID")
applicationSid := os.Getenv("TWILIO_TWIML_APP_SID")
apiKey := os.Getenv("TWILIO_API_KEY")
apiSecret := os.Getenv("TWILIO_API_SECRET")
identity := "fake123"

params := jwt.AccessTokenParams{
AccountSid: accountSid,
SigningKeySid: apiKey,
Secret: apiSecret,
Identity: identity,
}

jwtToken := jwt.CreateAccessToken(params)
voiceGrant := &jwt.VoiceGrant{
Incoming: jwt.Incoming{Allow: true},
Outgoing: jwt.Outgoing{
ApplicationSid: applicationSid,
ApplicationParams: "",
},
}

jwtToken.AddGrant(voiceGrant)
token, err := jwtToken.ToJwt()
```

Creating Capability Token for TaskRouter v1
```go
package main

import
(
"os"
"github.com/twilio/twilio-go/client/jwt/taskrouter"
)

Params = taskrouter.CapabilityTokenParams{
AccountSid: AccountSid,
AuthToken: AuthToken,
WorkspaceSid: WorkspaceSid,
ChannelID: TaskqueueSid,
}

capabilityToken := taskrouter.CreateCapabilityToken(Params)
token, err := capabilityToken.ToJwt()
```


## Local Usage

### Building
Expand Down
209 changes: 209 additions & 0 deletions client/jwt/access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package jwt

import (
"encoding/json"
"fmt"
"strconv"
"time"

. "github.com/twilio/twilio-go/client/jwt/util"
)

type AccessToken struct {
baseJwt *Jwt
// List of permissions that the token grants
Grants []BaseGrant `json:"grants,omitempty"`
// Twilio Account SID
AccountSid string `json:"account_sid,omitempty"`
// API key
SigningKeySid string `json:"signing_key_sid,omitempty"`
// User's identity
Identity string `json:"identity,omitempty"`
// User's region
Region interface{} `json:"region,omitempty"`
}

type AccessTokenParams struct {
// Twilio Account sid
AccountSid string
// The issuer of the token
SigningKeySid string
// The secret used to sign the token
Secret string
// Identity of the token issuer
Identity string
// User's Region
Region string
// Time in secs since epoch before which this JWT is invalid, defaults to now
Nbf float64
// Time to live of the JWT in seconds, defaults to 1 hour
Ttl float64
// Time in secs since epoch this JWT is valid for. Overrides ttl if provided.
ValidUntil float64
// Access permissions granted to this token
Grants []BaseGrant
}

func CreateAccessToken(params AccessTokenParams) AccessToken {
return AccessToken{
baseJwt: &Jwt{
SecretKey: params.Secret,
Issuer: params.SigningKeySid,
Subject: params.AccountSid,
Algorithm: HS256,
Nbf: params.Nbf,
Ttl: Max(params.Ttl, 3600),
ValidUntil: params.ValidUntil,
},
Grants: params.Grants,
AccountSid: params.AccountSid,
SigningKeySid: params.SigningKeySid,
Identity: params.Identity,
Region: params.Region,
}
}

func (token *AccessToken) Payload() map[string]interface{} {
if token.baseJwt.DecodedPayload == nil {
token.baseJwt.DecodedPayload = token.GeneratePayload()
}

return token.baseJwt.DecodedPayload
}

func (token *AccessToken) AddGrant(grant BaseGrant) {
if grant == nil {
panic("Grant to add is nil")
}
token.Grants = append(token.Grants, grant)
}

func (token *AccessToken) Headers() map[string]interface{} {
if token.baseJwt.DecodedHeaders == nil {
token.baseJwt.DecodedHeaders = token.generateHeaders()
}

return token.baseJwt.DecodedHeaders
}

func (token *AccessToken) generateHeaders() map[string]interface{} {
headers := make(map[string]interface{})
headers["cty"] = CType

if token.Region != "" {
headers["twr"] = token.Region
}

headers["alg"] = HS256
headers["typ"] = JWT

return headers
}

func (token *AccessToken) GeneratePayload() map[string]interface{} {
now := float64(time.Now().Unix())

grants := make(map[string]interface{})
for _, grant := range token.Grants {
grants[grant.Key()] = grant.ToPayload()
}

payload := map[string]interface{}{
"jti": fmt.Sprintf("%s-%s", token.SigningKeySid, strconv.Itoa(int(now))),
"grants": grants,
}

if token.Identity != "" {
val := payload["grants"].(map[string]interface{})
val["identity"] = token.Identity
}

payload["iss"] = token.baseJwt.Issuer
payload["exp"] = now + token.baseJwt.Ttl

if token.baseJwt.Nbf != 0 {
payload["nbf"] = token.baseJwt.Nbf
} else {
payload["nbf"] = now
}

if token.baseJwt.ValidUntil != 0 {
payload["exp"] = token.baseJwt.ValidUntil
}
if token.baseJwt.Subject != "" {
payload["sub"] = token.baseJwt.Subject
}

return payload
}

// Encode this JWT struct into a string.
// algorithm - algorithm used to encode the JWT that overrides the default
// ttl - specify ttl to override the default
func (token *AccessToken) ToJwt() (string, error) {
signedToken, err := token.baseJwt.ToJwt(token.generateHeaders, token.GeneratePayload)
if err != nil {
return "", err
}
return signedToken, nil
}

func decodeGrants(grants interface{}) []BaseGrant {
var decodedGrants []BaseGrant

for k, v := range grants.(map[string]interface{}) {
var grant BaseGrant
if data, err := json.Marshal(v); err == nil {
switch k {
case "chat":
grant = &ChatGrant{}
case "rtc":
grant = &ConversationsGrant{}
case "ip_messaging":
grant = &IpMessagingGrant{}
case "data_sync":
grant = &SyncGrant{}
case "task_router":
grant = &TaskRouterGrant{}
case "video":
grant = &VideoGrant{}
case "voice":
grant = &VoiceGrant{}
}

if errJson := json.Unmarshal(data, &grant); errJson == nil {
decodedGrants = append(decodedGrants, grant)
}
}
}

return decodedGrants
}

// Decode a JWT string into a Jwt struct.
// jwt - JWT string
// key - string key used to verify the JWT signature; if not provided, then validation is skipped
func (token *AccessToken) FromJwt(jwtStr string, key string) (*AccessToken, error) {
baseToken, err := token.baseJwt.FromJwt(jwtStr, key)
if err != nil {
return nil, err
}

decodedToken := &AccessToken{
baseJwt: baseToken,
Grants: decodeGrants(baseToken.Payload()["grants"]),
AccountSid: baseToken.Payload()["sub"].(string),
SigningKeySid: baseToken.Payload()["iss"].(string),
}

if val, ok := baseToken.Headers()["twr"]; ok {
decodedToken.Region = val
}
if val, ok := baseToken.Payload()["grants"]; ok {
if iVal, iOk := val.(map[string]interface{})["identity"]; iOk {
decodedToken.Identity = iVal.(string)
}
}

return decodedToken, nil
}
Loading

0 comments on commit ca601d2

Please sign in to comment.