Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jwt token signing and verification logic #116

Merged
merged 4 commits into from
Sep 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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