Skip to content

Commit 903d637

Browse files
authored
Merge pull request #1 from cikupin/redis-storage
Redis storage & jwt
2 parents fa7d02d + cffba98 commit 903d637

File tree

6 files changed

+183
-12
lines changed

6 files changed

+183
-12
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ _**Note** : This a modified version version of [EddyTravels/smooch](https://gith
66

77
## Additional Feature
88

9-
- Redis support as a centralized storage to store JWT token for supporting autoscaling environment.
9+
- Token expiration & its checking.
10+
- Renew token functionality whenever token is expired.
11+
- Redis support as a centralized storage to store JWT token for supporting autoscaling environment. Use redigo as redis library.
1012

1113
## Tips
1214

@@ -33,6 +35,7 @@ func main() {
3335
KeyID: os.Getenv("SMOOCH_KEY_ID"),
3436
Secret: os.Getenv("SMOOCH_SECRET"),
3537
VerifySecret: os.Getenv("SMOOCH_VERIFY_SECRET"),
38+
RedisPool: redisPool,
3639
})
3740
3841
if err != nil {

go.mod

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
module github.com/EddyTravels/smooch
1+
module github.com/kitabisa/smooch
22

33
require (
44
github.com/dgrijalva/jwt-go v3.2.0+incompatible
5+
github.com/gomodule/redigo v2.0.0+incompatible
56
github.com/stretchr/testify v1.3.0
67
)
8+
9+
go 1.13

go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
22
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
44
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
5+
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
6+
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
7+
github.com/kitabisa/smooch v0.1.0 h1:dS+ouObVdoNFVZWMIqULMr/VbQoYhsBuSUdmp0VjbcM=
58
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
69
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
710
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

jwt.go

+41
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package smooch
22

33
import (
4+
"time"
5+
46
jwt "github.com/dgrijalva/jwt-go"
57
)
68

9+
// JWTExpiration defines how many seconds jwt token is valid
10+
const JWTExpiration = 3600
11+
712
func GenerateJWT(scope string, keyID string, secret string) (string, error) {
813
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
914
"scope": scope,
15+
"exp": JWTExpiration,
1016
})
1117
token.Header = map[string]interface{}{
1218
"alg": "HS256",
@@ -16,3 +22,38 @@ func GenerateJWT(scope string, keyID string, secret string) (string, error) {
1622

1723
return token.SignedString([]byte(secret))
1824
}
25+
26+
// getJWTExpiration will get jwt expiration time
27+
func getJWTExpiration(jwtToken string, secret string) (int64, error) {
28+
claims := jwt.MapClaims{}
29+
30+
_, err := jwt.ParseWithClaims(jwtToken, &claims, func(t *jwt.Token) (interface{}, error) {
31+
return []byte(secret), nil
32+
})
33+
if err != nil {
34+
return -1, err
35+
}
36+
37+
expiredIn := claims["exp"].(int64) - time.Now().Unix()
38+
return expiredIn, nil
39+
}
40+
41+
// isJWTExpired will check whether Smooch JWT is expired or not.
42+
func isJWTExpired(jwtToken string, secret string) (bool, error) {
43+
_, err := jwt.ParseWithClaims(jwtToken, jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) {
44+
return []byte(secret), nil
45+
})
46+
47+
if err == nil {
48+
return false, nil
49+
}
50+
51+
switch err.(type) {
52+
case *jwt.ValidationError:
53+
vErr := err.(*jwt.ValidationError)
54+
if vErr.Errors == jwt.ValidationErrorExpired {
55+
return true, nil
56+
}
57+
}
58+
return false, err
59+
}

smooch.go

+91-10
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@ import (
1313
"os"
1414
"path"
1515
"strings"
16+
"sync"
17+
18+
"github.com/gomodule/redigo/redis"
19+
"github.com/kitabisa/smooch/storage"
1620
)
1721

1822
var (
1923
ErrUserIDEmpty = errors.New("user id is empty")
24+
ErrKeyIDEmpty = errors.New("key id is empty")
25+
ErrSecretEmpty = errors.New("secret is empty")
26+
ErrRedisNil = errors.New("redis pool is nil")
2027
ErrMessageNil = errors.New("message is nil")
2128
ErrMessageRoleEmpty = errors.New("message.Role is empty")
2229
ErrMessageTypeEmpty = errors.New("message.Type is empty")
2330
ErrVerifySecretEmpty = errors.New("verify secret is empty")
31+
ErrDecodeToken = errors.New("error decode token")
2432
)
2533

2634
const (
@@ -46,12 +54,15 @@ type Options struct {
4654
Logger Logger
4755
Region string
4856
HttpClient *http.Client
57+
RedisPool *redis.Pool
4958
}
5059

5160
type WebhookEventHandler func(payload *Payload)
5261

5362
type Client interface {
5463
Handler() http.Handler
64+
IsJWTExpired() (bool, error)
65+
RenewToken() (string, error)
5566
AddWebhookEventHandler(handler WebhookEventHandler)
5667
Send(userID string, message *Message) (*ResponsePayload, error)
5768
VerifyRequest(r *http.Request) bool
@@ -63,19 +74,34 @@ type Client interface {
6374
type smoochClient struct {
6475
mux *http.ServeMux
6576
appID string
66-
jwtToken string
77+
keyID string
78+
secret string
6779
verifySecret string
6880
logger Logger
6981
region string
7082
webhookEventHandlers []WebhookEventHandler
7183
httpClient *http.Client
84+
mtx sync.Mutex
85+
RedisStorage *storage.RedisStorage
7286
}
7387

7488
func New(o Options) (*smoochClient, error) {
89+
if o.KeyID == "" {
90+
return nil, ErrKeyIDEmpty
91+
}
92+
93+
if o.Secret == "" {
94+
return nil, ErrSecretEmpty
95+
}
96+
7597
if o.VerifySecret == "" {
7698
return nil, ErrVerifySecretEmpty
7799
}
78100

101+
if o.RedisPool == nil {
102+
return nil, ErrRedisNil
103+
}
104+
79105
if o.Mux == nil {
80106
o.Mux = http.NewServeMux()
81107
}
@@ -101,19 +127,24 @@ func New(o Options) (*smoochClient, error) {
101127
region = RegionEU
102128
}
103129

104-
jwtToken, err := GenerateJWT("app", o.KeyID, o.Secret)
105-
if err != nil {
106-
return nil, err
107-
}
108-
109130
sc := &smoochClient{
110131
mux: o.Mux,
111132
appID: o.AppID,
133+
keyID: o.KeyID,
134+
secret: o.Secret,
112135
verifySecret: o.VerifySecret,
113136
logger: o.Logger,
114137
region: region,
115138
httpClient: o.HttpClient,
116-
jwtToken: jwtToken,
139+
RedisStorage: storage.NewRedisStorage(o.RedisPool),
140+
}
141+
142+
_, err := sc.RedisStorage.GetTokenFromRedis()
143+
if err != nil {
144+
_, err := sc.RenewToken()
145+
if err != nil {
146+
return nil, err
147+
}
117148
}
118149

119150
sc.mux.HandleFunc(o.WebhookURL, sc.handle)
@@ -124,6 +155,36 @@ func (sc *smoochClient) Handler() http.Handler {
124155
return sc.mux
125156
}
126157

158+
// IsJWTExpired will check whether Smooch JWT is expired or not.
159+
func (sc *smoochClient) IsJWTExpired() (bool, error) {
160+
jwtToken, err := sc.RedisStorage.GetTokenFromRedis()
161+
if err != nil {
162+
if err == redis.ErrNil {
163+
return true, nil
164+
}
165+
return false, err
166+
}
167+
return isJWTExpired(jwtToken, sc.secret)
168+
}
169+
170+
// RenewToken will generate new Smooch JWT token.
171+
func (sc *smoochClient) RenewToken() (string, error) {
172+
sc.mtx.Lock()
173+
defer sc.mtx.Unlock()
174+
175+
jwtToken, err := GenerateJWT("app", sc.keyID, sc.secret)
176+
if err != nil {
177+
return "", err
178+
}
179+
180+
err = sc.RedisStorage.SaveTokenToRedis(jwtToken, JWTExpiration)
181+
if err != nil {
182+
return "", err
183+
}
184+
185+
return jwtToken, nil
186+
}
187+
127188
func (sc *smoochClient) AddWebhookEventHandler(handler WebhookEventHandler) {
128189
sc.webhookEventHandlers = append(sc.webhookEventHandlers, handler)
129190
}
@@ -326,17 +387,37 @@ func (sc *smoochClient) createRequest(
326387
buf *bytes.Buffer,
327388
header http.Header) (*http.Request, error) {
328389

390+
var req *http.Request
391+
var err error
392+
var jwtToken string
393+
329394
if header == nil {
330395
header = http.Header{}
331396
}
332397

333398
if header.Get(contentTypeHeaderKey) == "" {
334399
header.Set(contentTypeHeaderKey, contentTypeJSON)
335400
}
336-
header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", sc.jwtToken))
337401

338-
var req *http.Request
339-
var err error
402+
isExpired, err := sc.IsJWTExpired()
403+
if err != nil {
404+
return nil, err
405+
}
406+
407+
if isExpired {
408+
jwtToken, err = sc.RenewToken()
409+
if err != nil {
410+
return nil, err
411+
}
412+
} else {
413+
jwtToken, err = sc.RedisStorage.GetTokenFromRedis()
414+
if err != nil {
415+
return nil, err
416+
}
417+
}
418+
419+
header.Set(authorizationHeaderKey, fmt.Sprintf("Bearer %s", jwtToken))
420+
340421
if buf == nil {
341422
req, err = http.NewRequest(method, url, nil)
342423
} else {

storage/redis.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package storage
2+
3+
import (
4+
"github.com/gomodule/redigo/redis"
5+
)
6+
7+
// RedisStorage defines struct property for redis storage
8+
type RedisStorage struct {
9+
pool *redis.Pool
10+
jwtKey string
11+
}
12+
13+
// NewRedisStorage initializes new instance of redis storage
14+
func NewRedisStorage(p *redis.Pool) *RedisStorage {
15+
return &RedisStorage{
16+
pool: p,
17+
jwtKey: "smooch-jwt-token",
18+
}
19+
}
20+
21+
// SaveTokenToRedis will save jwt token to redis
22+
func (rs *RedisStorage) SaveTokenToRedis(token string, ttl int64) error {
23+
conn := rs.pool.Get()
24+
defer conn.Close()
25+
26+
_, err := conn.Do("SETEX", rs.jwtKey, ttl, token)
27+
return err
28+
}
29+
30+
// GetTokenFromRedis will retrieve jwt token from redis
31+
func (rs *RedisStorage) GetTokenFromRedis() (string, error) {
32+
conn := rs.pool.Get()
33+
defer conn.Close()
34+
35+
val, err := redis.String(conn.Do("GET", rs.jwtKey))
36+
if err != nil {
37+
return "", err
38+
}
39+
return val, nil
40+
}

0 commit comments

Comments
 (0)