├── .gitignore ├── .env ├── go.mod ├── README.md ├── middleware └── middleware.go ├── LICENSE ├── main.go ├── auth ├── auth.go └── token.go ├── handler └── handler.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REDIS_HOST=127.0.0.1 2 | REDIS_PORT=6379 3 | REDIS_PASSWORD= 4 | 5 | ACCESS_SECRET=98hbun98hsdfsdwesdfs 6 | REFRESH_SECRET=786dfdbjhsbsdfsdfsdf 7 | 8 | PORT=4000 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module jwt-app 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-gonic/gin v1.6.3 8 | github.com/go-redis/redis/v7 v7.4.0 9 | github.com/joho/godotenv v1.3.0 10 | github.com/myesui/uuid v1.0.0 // indirect 11 | github.com/twinj/uuid v1.0.0 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gophercon-jwt-repo 2 | This is the complete code for my talk on Gophercon Europe 2020, [here](https://www.youtube.com/watch?v=myIJZMxpfTE&list=PLtoVuM73AmsKnUvoFizEmvWo0BbegkSIG&index=12) 3 | Also a complete code for the article i wrote on Nexmo Developer Spotlight Program [here](https://www.nexmo.com/blog/2020/03/13/using-jwt-for-authentication-in-a-golang-application-dr) 4 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "jwt-app/auth" 6 | "net/http" 7 | ) 8 | 9 | func TokenAuthMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | err := auth.TokenValid(c.Request) 12 | if err != nil { 13 | c.JSON(http.StatusUnauthorized, "unauthorized") 14 | c.Abort() 15 | return 16 | } 17 | c.Next() 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Victor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-redis/redis/v7" 7 | "github.com/joho/godotenv" 8 | "jwt-app/auth" 9 | "jwt-app/handler" 10 | "jwt-app/middleware" 11 | "log" 12 | "net/http" 13 | "os" 14 | "os/signal" 15 | "time" 16 | ) 17 | 18 | func init() { 19 | if err := godotenv.Load(); err != nil { 20 | log.Print("No .env file found") 21 | } 22 | } 23 | 24 | func NewRedisDB(host, port, password string) *redis.Client { 25 | redisClient := redis.NewClient(&redis.Options{ 26 | Addr: host + ":" + port, 27 | Password: password, 28 | DB: 0, 29 | }) 30 | return redisClient 31 | } 32 | 33 | func main() { 34 | 35 | appAddr := ":" + os.Getenv("PORT") 36 | 37 | //redis details 38 | redis_host := os.Getenv("REDIS_HOST") 39 | redis_port := os.Getenv("REDIS_PORT") 40 | redis_password := os.Getenv("REDIS_PASSWORD") 41 | 42 | redisClient := NewRedisDB(redis_host, redis_port, redis_password) 43 | 44 | var rd = auth.NewAuth(redisClient) 45 | var tk = auth.NewToken() 46 | var service = handlers.NewProfile(rd, tk) 47 | 48 | var router = gin.Default() 49 | 50 | router.POST("/login", service.Login) 51 | router.POST("/todo", middleware.TokenAuthMiddleware(), service.CreateTodo) 52 | router.POST("/logout", middleware.TokenAuthMiddleware(), service.Logout) 53 | router.POST("/refresh", service.Refresh) 54 | 55 | srv := &http.Server{ 56 | Addr: appAddr, 57 | Handler: router, 58 | } 59 | go func() { 60 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 61 | log.Fatalf("listen: %s\n", err) 62 | } 63 | }() 64 | //Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds 65 | quit := make(chan os.Signal) 66 | signal.Notify(quit, os.Interrupt) 67 | <-quit 68 | log.Println("Shutdown Server ...") 69 | 70 | ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second) 71 | defer cancel() 72 | if err := srv.Shutdown(ctx); err != nil { 73 | log.Fatal("Server Shutdown:", err) 74 | } 75 | log.Println("Server exiting") 76 | } -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/go-redis/redis/v7" 7 | "time" 8 | ) 9 | 10 | type AuthInterface interface { 11 | CreateAuth(string, *TokenDetails) error 12 | FetchAuth(string) (string, error) 13 | DeleteRefresh(string) error 14 | DeleteTokens(*AccessDetails) error 15 | } 16 | 17 | type service struct { 18 | client *redis.Client 19 | } 20 | 21 | var _ AuthInterface = &service{} 22 | 23 | func NewAuth(client *redis.Client) *service { 24 | return &service{client: client} 25 | } 26 | 27 | type AccessDetails struct { 28 | TokenUuid string 29 | UserId string 30 | } 31 | 32 | type TokenDetails struct { 33 | AccessToken string 34 | RefreshToken string 35 | TokenUuid string 36 | RefreshUuid string 37 | AtExpires int64 38 | RtExpires int64 39 | } 40 | 41 | //Save token metadata to Redis 42 | func (tk *service) CreateAuth(userId string, td *TokenDetails) error { 43 | at := time.Unix(td.AtExpires, 0) //converting Unix to UTC(to Time object) 44 | rt := time.Unix(td.RtExpires, 0) 45 | now := time.Now() 46 | 47 | atCreated, err := tk.client.Set(td.TokenUuid, userId, at.Sub(now)).Result() 48 | if err != nil { 49 | return err 50 | } 51 | rtCreated, err := tk.client.Set(td.RefreshUuid, userId, rt.Sub(now)).Result() 52 | if err != nil { 53 | return err 54 | } 55 | if atCreated == "0" || rtCreated == "0" { 56 | return errors.New("no record inserted") 57 | } 58 | return nil 59 | } 60 | 61 | //Check the metadata saved 62 | func (tk *service) FetchAuth(tokenUuid string) (string, error) { 63 | userid, err := tk.client.Get(tokenUuid).Result() 64 | if err != nil { 65 | return "", err 66 | } 67 | return userid, nil 68 | } 69 | 70 | //Once a user row in the token table 71 | func (tk *service) DeleteTokens(authD *AccessDetails) error { 72 | //get the refresh uuid 73 | refreshUuid := fmt.Sprintf("%s++%s", authD.TokenUuid, authD.UserId) 74 | //delete access token 75 | deletedAt, err := tk.client.Del(authD.TokenUuid).Result() 76 | if err != nil { 77 | return err 78 | } 79 | //delete refresh token 80 | deletedRt, err := tk.client.Del(refreshUuid).Result() 81 | if err != nil { 82 | return err 83 | } 84 | //When the record is deleted, the return value is 1 85 | if deletedAt != 1 || deletedRt != 1 { 86 | return errors.New("something went wrong") 87 | } 88 | return nil 89 | } 90 | 91 | func (tk *service) DeleteRefresh(refreshUuid string) error { 92 | //delete refresh token 93 | deleted, err := tk.client.Del(refreshUuid).Result() 94 | if err != nil || deleted == 0 { 95 | return err 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/twinj/uuid" 8 | "net/http" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type tokenservice struct{} 15 | 16 | func NewToken() *tokenservice { 17 | return &tokenservice{} 18 | } 19 | 20 | type TokenInterface interface { 21 | CreateToken(userId string) (*TokenDetails, error) 22 | ExtractTokenMetadata(*http.Request) (*AccessDetails, error) 23 | } 24 | 25 | //Token implements the TokenInterface 26 | var _ TokenInterface = &tokenservice{} 27 | 28 | func (t *tokenservice) CreateToken(userId string) (*TokenDetails, error) { 29 | td := &TokenDetails{} 30 | td.AtExpires = time.Now().Add(time.Minute * 30).Unix() //expires after 30 min 31 | td.TokenUuid = uuid.NewV4().String() 32 | 33 | td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix() 34 | td.RefreshUuid = td.TokenUuid + "++" + userId 35 | 36 | var err error 37 | //Creating Access Token 38 | atClaims := jwt.MapClaims{} 39 | atClaims["access_uuid"] = td.TokenUuid 40 | atClaims["user_id"] = userId 41 | atClaims["exp"] = td.AtExpires 42 | at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) 43 | td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET"))) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | //Creating Refresh Token 49 | td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix() 50 | td.RefreshUuid = td.TokenUuid + "++" + userId 51 | 52 | rtClaims := jwt.MapClaims{} 53 | rtClaims["refresh_uuid"] = td.RefreshUuid 54 | rtClaims["user_id"] = userId 55 | rtClaims["exp"] = td.RtExpires 56 | rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims) 57 | 58 | td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET"))) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return td, nil 63 | } 64 | 65 | func TokenValid(r *http.Request) error { 66 | token, err := verifyToken(r) 67 | if err != nil { 68 | return err 69 | } 70 | if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { 71 | return err 72 | } 73 | return nil 74 | } 75 | 76 | func verifyToken(r *http.Request) (*jwt.Token, error) { 77 | tokenString := extractToken(r) 78 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 79 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 80 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 81 | } 82 | return []byte(os.Getenv("ACCESS_SECRET")), nil 83 | }) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return token, nil 88 | } 89 | 90 | //get the token from the request body 91 | func extractToken(r *http.Request) string { 92 | bearToken := r.Header.Get("Authorization") 93 | strArr := strings.Split(bearToken, " ") 94 | if len(strArr) == 2 { 95 | return strArr[1] 96 | } 97 | return "" 98 | } 99 | 100 | func extract(token *jwt.Token) (*AccessDetails, error) { 101 | 102 | claims, ok := token.Claims.(jwt.MapClaims) 103 | if ok && token.Valid { 104 | accessUuid, ok := claims["access_uuid"].(string) 105 | userId, userOk := claims["user_id"].(string) 106 | if ok == false || userOk == false { 107 | return nil, errors.New("unauthorized") 108 | } else { 109 | return &AccessDetails{ 110 | TokenUuid: accessUuid, 111 | UserId: userId, 112 | }, nil 113 | } 114 | } 115 | return nil, errors.New("something went wrong") 116 | } 117 | 118 | func (t *tokenservice) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) { 119 | token, err := verifyToken(r) 120 | if err != nil { 121 | return nil, err 122 | } 123 | acc, err := extract(token) 124 | if err != nil { 125 | return nil, err 126 | } 127 | return acc, nil 128 | } 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/dgrijalva/jwt-go" 6 | "github.com/gin-gonic/gin" 7 | "jwt-app/auth" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | // ProfileHandler struct 13 | type profileHandler struct { 14 | rd auth.AuthInterface 15 | tk auth.TokenInterface 16 | } 17 | 18 | func NewProfile(rd auth.AuthInterface, tk auth.TokenInterface) *profileHandler { 19 | return &profileHandler{rd, tk} 20 | } 21 | 22 | type User struct { 23 | ID string `json:"id"` 24 | Username string `json:"username"` 25 | Password string `json:"password"` 26 | } 27 | //In memory user 28 | var user = User { 29 | ID: "1", 30 | Username: "username", 31 | Password: "password", 32 | } 33 | 34 | type Todo struct { 35 | UserID string `json:"user_id"` 36 | Title string `json:"title"` 37 | Body string `json:"body"` 38 | } 39 | 40 | func (h *profileHandler) Login(c *gin.Context) { 41 | var u User 42 | if err := c.ShouldBindJSON(&u); err != nil { 43 | c.JSON(http.StatusUnprocessableEntity, "Invalid json provided") 44 | return 45 | } 46 | //compare the user from the request, with the one we defined: 47 | if user.Username != u.Username || user.Password != u.Password { 48 | c.JSON(http.StatusUnauthorized, "Please provide valid login details") 49 | return 50 | } 51 | ts, err := h.tk.CreateToken(user.ID) 52 | if err != nil { 53 | c.JSON(http.StatusUnprocessableEntity, err.Error()) 54 | return 55 | } 56 | saveErr := h.rd.CreateAuth(user.ID, ts) 57 | if saveErr != nil { 58 | c.JSON(http.StatusUnprocessableEntity, saveErr.Error()) 59 | return 60 | } 61 | tokens := map[string]string{ 62 | "access_token": ts.AccessToken, 63 | "refresh_token": ts.RefreshToken, 64 | } 65 | c.JSON(http.StatusOK, tokens) 66 | } 67 | 68 | func (h *profileHandler) Logout(c *gin.Context) { 69 | //If metadata is passed and the tokens valid, delete them from the redis store 70 | metadata, _ := h.tk.ExtractTokenMetadata(c.Request) 71 | if metadata != nil { 72 | deleteErr := h.rd.DeleteTokens(metadata) 73 | if deleteErr != nil { 74 | c.JSON(http.StatusBadRequest, deleteErr.Error()) 75 | return 76 | } 77 | } 78 | c.JSON(http.StatusOK, "Successfully logged out") 79 | } 80 | 81 | func (h *profileHandler) CreateTodo(c *gin.Context) { 82 | var td Todo 83 | if err := c.ShouldBindJSON(&td); err != nil { 84 | c.JSON(http.StatusUnprocessableEntity, "invalid json") 85 | return 86 | } 87 | metadata, err := h.tk.ExtractTokenMetadata(c.Request) 88 | if err != nil { 89 | c.JSON(http.StatusUnauthorized, "unauthorized") 90 | return 91 | } 92 | userId, err := h.rd.FetchAuth(metadata.TokenUuid) 93 | if err != nil { 94 | c.JSON(http.StatusUnauthorized, "unauthorized") 95 | return 96 | } 97 | td.UserID = userId 98 | 99 | //you can proceed to save the to a database 100 | 101 | c.JSON(http.StatusCreated, td) 102 | } 103 | 104 | 105 | func (h *profileHandler) Refresh(c *gin.Context) { 106 | mapToken := map[string]string{} 107 | if err := c.ShouldBindJSON(&mapToken); err != nil { 108 | c.JSON(http.StatusUnprocessableEntity, err.Error()) 109 | return 110 | } 111 | refreshToken := mapToken["refresh_token"] 112 | 113 | //verify the token 114 | token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { 115 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 116 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 117 | } 118 | return []byte(os.Getenv("REFRESH_SECRET")), nil 119 | }) 120 | //if there is an error, the token must have expired 121 | if err != nil { 122 | c.JSON(http.StatusUnauthorized, "Refresh token expired") 123 | return 124 | } 125 | //is token valid? 126 | if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { 127 | c.JSON(http.StatusUnauthorized, err) 128 | return 129 | } 130 | //Since token is valid, get the uuid: 131 | claims, ok := token.Claims.(jwt.MapClaims) //the token claims should conform to MapClaims 132 | if ok && token.Valid { 133 | refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string 134 | if !ok { 135 | c.JSON(http.StatusUnprocessableEntity, err) 136 | return 137 | } 138 | userId, roleOk := claims["user_id"].(string) 139 | if roleOk == false { 140 | c.JSON(http.StatusUnprocessableEntity, "unauthorized") 141 | return 142 | } 143 | //Delete the previous Refresh Token 144 | delErr := h.rd.DeleteRefresh(refreshUuid) 145 | if delErr != nil { //if any goes wrong 146 | c.JSON(http.StatusUnauthorized, "unauthorized") 147 | return 148 | } 149 | //Create new pairs of refresh and access tokens 150 | ts, createErr := h.tk.CreateToken(userId) 151 | if createErr != nil { 152 | c.JSON(http.StatusForbidden, createErr.Error()) 153 | return 154 | } 155 | //save the tokens metadata to redis 156 | saveErr := h.rd.CreateAuth(userId, ts) 157 | if saveErr != nil { 158 | c.JSON(http.StatusForbidden, saveErr.Error()) 159 | return 160 | } 161 | tokens := map[string]string{ 162 | "access_token": ts.AccessToken, 163 | "refresh_token": ts.RefreshToken, 164 | } 165 | c.JSON(http.StatusCreated, tokens) 166 | } else { 167 | c.JSON(http.StatusUnauthorized, "refresh expired") 168 | } 169 | } 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 5 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 6 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 9 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 10 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 11 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 12 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 13 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 14 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 15 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 16 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 17 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 18 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 19 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 20 | github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o= 21 | github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4= 22 | github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 25 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 27 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 29 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 30 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 31 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 32 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 33 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 34 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 35 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 36 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 37 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 38 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 41 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 42 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 43 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 44 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 45 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 47 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 48 | github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= 49 | github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= 50 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 51 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 52 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 53 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 54 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 58 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 59 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 60 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 61 | github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk= 62 | github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY= 63 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 64 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 65 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 66 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 69 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 70 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 71 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= 75 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 77 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 79 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 80 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 81 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 84 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 86 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 87 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 88 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 89 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 90 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 91 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 92 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 93 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 94 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 95 | --------------------------------------------------------------------------------