diff --git a/auth/api_key.go b/auth/api_key.go index 935505bee993a4594db461c61574eebe34b15eee..739dfc08b1d972ec7b2058d0e9358047d3610794 100644 --- a/auth/api_key.go +++ b/auth/api_key.go @@ -20,17 +20,10 @@ func GenerateNewApiKey() string { return uniqueKey } -// GetApiKeyFromHeaders checks if a bearer token is passed as part of the Authorization header and returns that key +// GetApiKeyFromHeaders checks if a bearer token is passed as part of the Authorization header and returns that key. +// NOTE: This function is deprecated. It simply calls the more generic GetBearerTokenFromHeaders. func GetApiKeyFromHeaders(headers map[string]string) string { - key := headers["authorization"] - if key == "" { - key = headers["Authorization"] - } - if strings.HasPrefix(strings.ToLower(key), "bearer ") { - key = strings.TrimPrefix(strings.ToLower(key), "bearer") - return strings.TrimSpace(key) - } - return "" + return GetBearerTokenFromHeaders(headers) } // MaskAPIKey masks an API key in the form "abc***xyz" diff --git a/auth/common.go b/auth/common.go new file mode 100644 index 0000000000000000000000000000000000000000..d411101a2d2e96b1489bf5bbab3e4d1728e16190 --- /dev/null +++ b/auth/common.go @@ -0,0 +1,34 @@ +package auth + +import ( + "golang.org/x/crypto/bcrypt" + "strings" +) + +// GetBearerTokenFromHeaders checks if a bearer token is passed as part of the Authorization header and returns that key +func GetBearerTokenFromHeaders(headers map[string]string) string { + headerValue := headers["authorization"] + if headerValue == "" { + headerValue = headers["Authorization"] + } + if strings.HasPrefix(strings.ToLower(headerValue), "bearer ") { + headerValues := strings.Split(headerValue, " ") + return strings.TrimSpace(headerValues[1]) + } + return "" +} + +// HashPassword returns a hashed version of the provided password. +func HashPassword(password string) (string, error) { + encryptedBytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + if err != nil { + return "", err + } + return string(encryptedBytes), nil +} + +// PasswordIsCorrect checks whether the password is correct by validating it against the hashed password. +func PasswordIsCorrect(password string, hashedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +} diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 0000000000000000000000000000000000000000..2f9c35ef01dd5664fc0f7550cc4e297ccb620db1 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,93 @@ +package auth + +import ( + "encoding/json" + "github.com/golang-jwt/jwt/v4" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/date_utils" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" + "net/http" + "time" +) + +type JsonWebToken struct { + UserID string `json:"user_id"` + Password string `json:"password"` + ProviderID int64 `json:"provider_id,omitempty"` + ExpiryDate time.Time `json:"expiry_date"` +} + +// GenerateJWT takes the payload and generates a signed JWT using the provided secret +func GenerateJWT(payload JsonWebToken, secret []byte) (string, error) { + // Convert the JsonWebToken to a map[string]interface{} + tokenBytes, err := json.Marshal(payload) + if err != nil { + return "", err + } + + tokenMap := make(map[string]interface{}) + err = json.Unmarshal(tokenBytes, &tokenMap) + if err != nil { + return "", err + } + + // Create the signed token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(tokenMap)) + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// ValidateJWT parses the JWT and validates that it is signed correctly +func ValidateJWT(tokenString string, secret []byte) (JsonWebToken, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Validate the signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return secret, nil + }) + if err != nil { + return JsonWebToken{}, err + } + if token == nil { + return JsonWebToken{}, errors.Error("could not get token from token string") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok || !token.Valid { + return JsonWebToken{}, errors.Error("invalid token") + } + + // Convert the MapClaims to a JsonWebToken + claimsBytes, err := json.Marshal(claims) + if err != nil { + return JsonWebToken{}, err + } + + var jsonWebToken JsonWebToken + err = json.Unmarshal(claimsBytes, &jsonWebToken) + if err != nil { + return JsonWebToken{}, err + } + + // Validate the expiry date + if jsonWebToken.ExpiryDate.Before(date_utils.CurrentDate()) { + return jsonWebToken, errors.Error("token has expired") + } + + return jsonWebToken, nil +} + +// LoginWithPassword checks that the provided password is correct. If the password is correct, a signed JWT is returned +// using the provided encryption key. +func LoginWithPassword(password string, hashedPassword string, jsonWebToken JsonWebToken, jwtEncryptionKey string) (string, error) { + if PasswordIsCorrect(password, hashedPassword) { + return GenerateJWT(jsonWebToken, []byte(jwtEncryptionKey)) + } + + return "", errors.HTTPWithMsg(http.StatusBadRequest, "password is incorrect") +} diff --git a/auth/jwt_test.go b/auth/jwt_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8fe270b56a56dda2482976d7bd89f5e2dbf1146a --- /dev/null +++ b/auth/jwt_test.go @@ -0,0 +1,68 @@ +package auth + +import ( + "fmt" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/date_utils" + "testing" + "time" +) + +func TestJWT(t *testing.T) { + secret := []byte("THISISTHESECRETKEY") + + jwToken := JsonWebToken{ + UserID: "janoadmin", + Password: "Password123", + ProviderID: 1, + ExpiryDate: date_utils.CurrentDate().Add(time.Second * 1), + } + + // Validate before expire + fmt.Println("TEST VALID JWT") + jwTokenString, err := GenerateJWT(jwToken, secret) + if err != nil { + fmt.Println("!!!!FAIL", err.Error()) + t.FailNow() + } + validatedToken, err := ValidateJWT(jwTokenString, secret) + if err != nil { + fmt.Println("!!!!FAIL", err.Error()) + t.Fail() + } else { + fmt.Println(" SUCCESS", validatedToken) + } + + // Validate invalid signature + fmt.Println("TEST INVALID JWT SIGNATURE") + jwTokenString, err = GenerateJWT(jwToken, []byte("INCORRECTSECRETVALUE")) + if err != nil { + fmt.Println("!!!!FAIL", err.Error()) + t.FailNow() + } + validatedToken, err = ValidateJWT(jwTokenString, secret) + if err == nil { + fmt.Println("!!!!FAIL") + t.Fail() + } else { + fmt.Println(" SUCCESS", err.Error()) + } + + time.Sleep(time.Second * 1) + + // Validate after expire + fmt.Println("TEST EXPIRED JWT") + jwTokenString, err = GenerateJWT(jwToken, secret) + if err != nil { + fmt.Println("!!!!FAIL", err.Error()) + t.FailNow() + } + validatedToken, err = ValidateJWT(jwTokenString, secret) + if err == nil { + fmt.Println("!!!!FAIL") + t.Fail() + } else { + fmt.Println(" SUCCESS", err.Error()) + } + + fmt.Println("Done!") +} diff --git a/go.mod b/go.mod index 9381a774add938deea3931118932f7d7652b92ae..b2619ee064f058eb0cc0cff433f05bdf218d34e2 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-errors/errors v1.4.1 // indirect github.com/go-pg/zerochecker v0.2.0 // indirect + github.com/golang-jwt/jwt/v4 v4.4.3 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index c9a81db708f321973513b60c8b9cc478bd7d285b..2d3ed86b232212a893bd3c91d4fe2431fbfe754c 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iy github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=