This commit is contained in:
Ramiro Paz
2026-03-09 15:09:06 -03:00
parent 8299f8bc96
commit 0e8fe168ef
85 changed files with 14079 additions and 0 deletions

347
src/common/jwttoken/jwt.go Normal file
View File

@ -0,0 +1,347 @@
// Package jwttoken encrypts and decrypts tokens using JWT
package jwttoken
import (
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/rs/zerolog"
zlog "github.com/rs/zerolog/log"
"quantex.com/qfixdpl/src/app"
"quantex.com/qfixdpl/src/app/version"
"quantex.com/qfixdpl/src/common/tracerr"
)
const (
postFix = "_QUANTEX_SECRET_KEY"
errAuthInvalidJWT = "invalid JWT token at %s"
errAuthMissingTokenClaim = "missing token claim %s at %s"
errAuthTokenNotSigned = "token could not be signed at Encrypt: %w"
errAuthMissingToken = "missing authentication token for service at %s: %s"
errAuthExpiredToken = "authentication token expired at %s"
errAuthExpClaim = "expiration claim is missing or invalid at %s"
errAuthSecretKeyNotFound = "secret key %s not found in environment"
errAuthTokenNotParsable = "token could not be parsed at %s:%w "
errAuthServiceNameEmpty = "service name cannot be empty at %s - received: %s"
claimToken = "token"
claimPermissions = "permissions"
claimIss = "iss"
claimExp = "exp"
claimIat = "iat"
)
var claimsToValidate = []string{claimToken, claimIss} //nolint:gochecknoglobals // Set claims to validate
var log zerolog.Logger //nolint
var secretCache sync.Map //nolint:gochecknoglobals // Cache secrets after first lookup
func init() {
log = zlog.With().Str("gtag", "secure_token").Logger().Level(zerolog.InfoLevel)
}
// Validate decrypts and validates a JWT token and its claims.
// It returns the AuthorizedService associated with the token if valid.
// An error is returned if the token is invalid or any validation step fails.
// The service parameter specifies the expected issuer of the token.
func Validate(service, token string, authServices map[string]app.AuthorizedService) (*app.AuthorizedService, error) {
claims, err := decrypt(service, token)
if err != nil {
err := tracerr.Errorf("JWT Token could not be decrypted: %w", err)
log.Error().Msg(err.Error())
return nil, err
}
ok, err := validateJWTClaims(claims)
if err != nil || !ok {
err := tracerr.Errorf("invalid claims: %w", err)
log.Error().Msg(err.Error())
return nil, err
}
auth, err := serviceAuth(claims, authServices)
if err != nil {
err := tracerr.Errorf("service auth validation failed: %w", err)
log.Error().Msg(err.Error())
return nil, err
}
t, ok := claims[claimToken].(string)
if !ok {
err := tracerr.Errorf("token claim is not a string")
log.Error().Msg(err.Error())
return nil, err
}
auth.Token = &t
return auth, nil
}
// Encrypt creates a JWT token string from the provided ExtAuth information.
// It returns the signed JWT token string or an error if the process fails.
// The auth parameter contains the necessary information for token creation.
func Encrypt(auth app.ExtAuth) (string, error) {
if auth.Name == "" {
err := tracerr.Errorf("auth.Name cannot be empty at Encrypt")
log.Error().Msg(err.Error())
return "", err
}
claims := jwt.MapClaims{
claimToken: auth.Token,
claimIss: version.AppName,
claimIat: time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secret, err := findSecret(auth.Name)
if secret == nil || len(secret) == 0 || err != nil {
err := tracerr.Errorf(errAuthMissingToken, "Encrypt", auth.Name)
log.Error().Msg(err.Error())
return "", err
}
signedTkn, err := token.SignedString(secret)
if err != nil {
err = tracerr.Errorf(errAuthTokenNotSigned, err)
log.Error().Msg(err.Error())
return "", err
}
return signedTkn, nil
}
// IsJWT checks if a token string is a valid JWT format.
// JWT tokens have exactly 3 parts separated by dots (header.payload.signature).
func IsJWT(token string) bool {
// Remove any whitespace
token = strings.TrimSpace(token)
// JWT has exactly 3 parts separated by dots
parts := strings.Split(token, ".")
if len(parts) != 3 {
return false
}
// Each part should be non-empty (base64url encoded)
for _, part := range parts {
if part == "" {
return false
}
}
return true
}
// decrypt decrypts a JWT token string using the secret associated with the given service.
// It returns the JWT claims if decryption is successful, or an error otherwise.
// The service parameter specifies the expected issuer of the token.
func decrypt(service, token string) (jwt.MapClaims, error) {
if service == "" {
err := tracerr.Errorf(errAuthServiceNameEmpty, "Decrypt", service)
log.Error().Msg(err.Error())
return jwt.MapClaims{}, err
}
secret, err := findSecret(service)
if secret == nil || len(secret) == 0 || err != nil {
err := tracerr.Errorf(errAuthMissingToken, "Decrypt", service)
log.Error().Msg(err.Error())
return jwt.MapClaims{}, err
}
tkn, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
if t.Method.Alg() != jwt.SigningMethodHS256.Alg() {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return secret, nil
})
if err != nil {
err = tracerr.Errorf(errAuthTokenNotParsable, "Decrypt", err)
log.Error().Msg(err.Error())
return jwt.MapClaims{}, err
}
claims, ok := tkn.Claims.(jwt.MapClaims)
if !ok {
err := tracerr.Errorf(errAuthInvalidJWT, "Decrypt")
log.Error().Msg(err.Error())
return jwt.MapClaims{}, err
}
return claims, nil
}
// serviceAuth validates the service based on JWT claims and authorized services.
// It returns the AuthorizedService if validation is successful, or an error otherwise.
// The services parameter contains the map of authorized services to validate against.
func serviceAuth(claims jwt.MapClaims, services map[string]app.AuthorizedService) (out *app.AuthorizedService, err error) {
issuer, ok := claims[claimIss].(string)
if !ok {
err := tracerr.Errorf("issuer claim is not a string")
log.Error().Msg(err.Error())
return nil, err
}
service, ok := services[issuer]
if !ok {
err = tracerr.Errorf("Unknown service attempted access - issuer: %s", issuer)
log.Warn().Str("issuer", issuer).Msg(err.Error())
return nil, err
}
// TODO: change required permission as needed
ok = service.HasPermissions(app.ServicePermissionFullAccess)
if !ok {
err = tracerr.Errorf("Service without required permissions attempted access")
log.Warn().Str("issuer", issuer).Msg(err.Error())
return nil, err
}
return &service, nil
}
// findSecret retrieves the secret key for a given application name from environment variables.
// It caches the secret after the first lookup for performance.
// It returns the secret as a byte slice or an error if not found.
func findSecret(appName string) ([]byte, error) {
key := strings.ToUpper(appName) + postFix
if k, ok := secretCache.Load(key); ok {
if b, ok := k.([]byte); ok {
return b, nil
}
}
secret := os.Getenv(key)
if secret == "" {
err := tracerr.Errorf(errAuthSecretKeyNotFound, key)
return nil, err
}
secretCache.Store(key, []byte(secret))
return []byte(secret), nil
}
// validateJWTClaims checks the presence and validity of required JWT claims.
// It returns true if all claims are valid, or an error otherwise.
func validateJWTClaims(claims jwt.MapClaims) (ok bool, err error) {
for _, claim := range claimsToValidate {
ok, err = validateClaims(claims, claim)
if err != nil || !ok {
err := tracerr.Errorf("error %s token at ValidateClaims: %w", claim, err)
log.Error().Msg(err.Error())
return false, err
}
}
return true, nil
}
// validateClaims validates a specific JWT claim based on its type.
func validateClaims(claims jwt.MapClaims, claim string) (ok bool, err error) {
switch claim {
case claimExp:
return validateExpiration(claims)
case claimToken:
return validateToken(claims)
case claimIss:
return validateIssuer(claims)
default:
err := fmt.Errorf("unknown claim %s at validateTokenClaims", claim)
log.Error().Msg(err.Error())
return false, err
}
}
// TODO: when needed add claimExp to claimsToValidate var to implement.
func validateExpiration(claims jwt.MapClaims) (ok bool, err error) {
exp, ok := claims[claimExp].(float64)
if !ok {
err := fmt.Errorf(errAuthExpClaim, "validateExpiration")
log.Error().Msg(err.Error())
return false, nil
}
if int64(exp) < time.Now().Unix() {
err := fmt.Errorf(errAuthExpiredToken, "validateExpiration")
log.Error().Msg(err.Error())
return false, err
}
return true, nil
}
// validateToken checks for the presence of the token claim in the JWT claims.
func validateToken(claims jwt.MapClaims) (ok bool, err error) {
_, ok = claims[claimToken].(string)
if !ok {
err = tracerr.Errorf(errAuthMissingTokenClaim, claimToken, "validateToken")
log.Error().Msg(err.Error())
return false, err
}
return true, nil
}
// validateIssuer checks for the presence of the issuer claim in the JWT claims.
func validateIssuer(claims jwt.MapClaims) (ok bool, err error) {
_, ok = claims[claimIss].(string)
if !ok {
err = tracerr.Errorf(errAuthMissingTokenClaim, claimIss, "validateIssuer")
log.Error().Msg(err.Error())
return false, err
}
return true, nil
}