Browse Source

add token

sainw 2 years ago
parent
commit
7c3cf32dc7
4 changed files with 206 additions and 0 deletions
  1. 2 0
      go.mod
  2. 4 0
      go.sum
  3. 120 0
      jwt/token.go
  4. 80 0
      jwt/token_test.go

+ 2 - 0
go.mod

@@ -7,9 +7,11 @@ require (
 	cloud.google.com/go/firestore v1.6.0 // indirect
 	cloud.google.com/go/storage v1.18.2 // indirect
 	firebase.google.com/go v3.13.0+incompatible // indirect
+	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.6 // indirect
+	github.com/google/uuid v1.3.0 // indirect
 	github.com/googleapis/gax-go/v2 v2.1.1 // indirect
 	go.opencensus.io v0.23.0 // indirect
 	golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect

+ 4 - 0
go.sum

@@ -77,6 +77,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -145,6 +147,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=

+ 120 - 0
jwt/token.go

@@ -0,0 +1,120 @@
+package token
+
+import (
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"math/big"
+	"time"
+
+	"github.com/golang-jwt/jwt"
+	"github.com/google/uuid"
+)
+
+var (
+	ErrInvalidToken  = errors.New("token is invalid")
+	ErrExpiredToken  = errors.New("token has expired")
+	ErrTokenUnknown  = errors.New("token unknown error")
+	ErrNotEvenAToken = errors.New("not even a token")
+	ErrUnhandleToken = errors.New("unhandel token")
+)
+
+type Maker interface {
+	CreateToken(issuer, username string, duration time.Duration) (string, error)
+	VerifyToken(token string) (*CustomClaims, error)
+}
+
+type CustomClaims struct {
+	UserName string `json:"user_name"`
+	jwt.StandardClaims
+}
+
+func NewCustomClaim(issuer, username string, duration time.Duration) (*CustomClaims, error) {
+	tokenID, err := uuid.NewRandom()
+	if err != nil {
+		return nil, err
+	}
+
+	claims := CustomClaims{
+		username,
+		jwt.StandardClaims{
+			Id:        tokenID.String(),
+			IssuedAt:  time.Now().Unix(),
+			ExpiresAt: time.Now().Add(duration).Unix(),
+			Issuer:    issuer,
+		},
+	}
+	return &claims, nil
+}
+
+type JWTMaker struct {
+	secretKey string
+}
+
+const minSecretKeySize = 32
+
+func NewJWTMaker(secretKey string) (Maker, error) {
+	if len(secretKey) < minSecretKeySize {
+		return nil, fmt.Errorf("invalid key size: must be at least %d characters", minSecretKeySize)
+	}
+	return &JWTMaker{secretKey}, nil
+}
+
+func (maker *JWTMaker) CreateToken(issuer, username string, duration time.Duration) (string, error) {
+	customClaim, err := NewCustomClaim(issuer, username, duration)
+	if err != nil {
+		return "", err
+	}
+
+	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, customClaim)
+	return jwtToken.SignedString([]byte(maker.secretKey))
+}
+
+// VerifyToken checks if the token is valid or not
+func (maker *JWTMaker) VerifyToken(token string) (*CustomClaims, error) {
+	keyFunc := func(token *jwt.Token) (interface{}, error) {
+		_, ok := token.Method.(*jwt.SigningMethodHMAC)
+		if !ok {
+			return nil, ErrInvalidToken
+		}
+		return []byte(maker.secretKey), nil
+	}
+
+	jwtToken, err := jwt.ParseWithClaims(token, &CustomClaims{}, keyFunc)
+
+	if err != nil {
+		if verr, ok := err.(*jwt.ValidationError); ok {
+			if verr.Errors&jwt.ValidationErrorMalformed != 0 {
+				return nil, ErrNotEvenAToken
+			} else if verr.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
+				return nil, ErrExpiredToken
+			} else {
+				return nil, ErrUnhandleToken
+			}
+		} else {
+			return nil, ErrTokenUnknown
+		}
+	}
+
+	payload, ok := jwtToken.Claims.(*CustomClaims)
+	if !ok {
+		return nil, ErrInvalidToken
+	}
+
+	return payload, nil
+}
+
+func GenerateSecretKey() (string, error) {
+	n := 120
+	const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
+	ret := make([]byte, n)
+	for i := 0; i < n; i++ {
+		num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
+		if err != nil {
+			return "", err
+		}
+		ret[i] = letters[num.Int64()]
+	}
+
+	return string(ret), nil
+}

+ 80 - 0
jwt/token_test.go

@@ -0,0 +1,80 @@
+package token
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+)
+
+func TestToken(t *testing.T) {
+	secret := uuid.New().String()
+
+	// create maker
+	issuer := "daisy"
+	user := "user1"
+	maker, err := NewJWTMaker(secret)
+	if err != nil {
+		t.Errorf("creating token maker error %s", err.Error())
+	}
+	token, err := maker.CreateToken(issuer, user, time.Second*10)
+	if err != nil {
+		t.Errorf("creating token error %s", err.Error())
+	}
+
+	// create verifier
+	verifier, err := NewJWTMaker(secret)
+	if err != nil {
+		t.Errorf("creating token maker verifier error %s", err.Error())
+	}
+	customClaim, err := verifier.VerifyToken(token)
+	if err != nil {
+		t.Errorf("verify token error %s", err.Error())
+	}
+	if customClaim.Issuer != issuer {
+		t.Errorf("issuer mismatch ")
+	}
+	if customClaim.UserName != user {
+		t.Errorf("user mismatch ")
+	}
+}
+
+func TestTokenExpiration(t *testing.T) {
+	secret := uuid.New().String()
+
+	// create maker
+	issuer := "daisy"
+	user := "user1"
+	maker, err := NewJWTMaker(secret)
+	if err != nil {
+		t.Errorf("creating token maker error %s", err.Error())
+	}
+	token, err := maker.CreateToken(issuer, user, time.Second*2)
+	if err != nil {
+		t.Errorf("creating token error %s", err.Error())
+	}
+
+	time.Sleep(time.Second * 3)
+	// create verifier
+	verifier, err := NewJWTMaker(secret)
+	if err != nil {
+		t.Errorf("creating token maker verifier error %s", err.Error())
+	}
+	_, err = verifier.VerifyToken(token)
+	if err == nil {
+		t.Errorf("verify token should return error")
+	}
+}
+
+func TestSecretKeyGenerator(t *testing.T) {
+	key, err := GenerateSecretKey()
+	fmt.Println("key:", key)
+	// t.Logf("key:%s\n", key)
+	if err != nil {
+		t.Errorf("generate secret key error %s", err.Error())
+	}
+	if len(key) != 120 {
+		t.Errorf("secret key length, expected %d, got %d", 120, len(key))
+	}
+}