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=