package auth import ( "errors" "fmt" "time" "github.com/FernandoJVideira/api_tmpl/internal/config" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) // JWTMaker is a JSON Web Token maker type JWTMaker struct { secretKey string config config.Config } func NewJWTClaims(payload *Payload, config config.Config) jwt.Claims { return &jwt.MapClaims{ "iat": jwt.NewNumericDate(payload.IssuedAt), "iss": config.TokenISS, "exp": jwt.NewNumericDate(payload.ExpiredAt), "nbf": jwt.NewNumericDate(payload.IssuedAt), "aud": []string{config.TokenAUD}, "sub": payload.Username, "role": payload.Role, "jti": payload.ID.String(), } } // NewJWTMaker creates a new JWTMaker func NewJWTMaker(config config.Config) (Maker, error) { if len(config.TokenKey) < config.MinTokenSize { return nil, fmt.Errorf("invalid key size: must be at least %d characters", config.MinTokenSize) } return &JWTMaker{secretKey: config.TokenKey, config: config}, nil } func (maker *JWTMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) { payload, err := NewPayload(username, role, duration) if err != nil { return "", nil, err } jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, NewJWTClaims(payload, maker.config)) signedString, err := jwtToken.SignedString([]byte(maker.secretKey)) return signedString, payload, err } func (maker *JWTMaker) VerifyToken(token string) (*Payload, error) { payloadClaims := jwt.MapClaims{} keyFunc := func(token *jwt.Token) (any, error) { _, ok := token.Method.(*jwt.SigningMethodHMAC) if !ok { return nil, ErrInvalidToken } return []byte(maker.secretKey), nil } jwtToken, err := jwt.ParseWithClaims(token, payloadClaims, keyFunc) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, ErrExpiredToken } else if errors.Is(err, ErrInvalidToken) { return nil, ErrInvalidToken } else { return nil, err } } payloadClaims, ok := jwtToken.Claims.(jwt.MapClaims) if !ok { return nil, ErrInvalidToken } payload, err := parseTypes(payloadClaims) if err != nil { return nil, err } return payload, nil } func parseTypes(claims jwt.MapClaims) (*Payload, error) { // Safe type assertions with error checking jti, ok := claims["jti"].(string) if !ok { return nil, ErrInvalidToken } id, err := uuid.Parse(jti) if err != nil { return nil, ErrInvalidToken } username, ok := claims["sub"].(string) if !ok { return nil, ErrInvalidToken } role, ok := claims["role"].(string) if !ok { return nil, ErrInvalidToken } // Parse numeric dates nbf, ok := claims["nbf"].(float64) if !ok { return nil, ErrInvalidToken } exp, ok := claims["exp"].(float64) if !ok { return nil, ErrInvalidToken } return &Payload{ ID: id, Username: username, Role: role, IssuedAt: time.Unix(int64(nbf), 0), ExpiredAt: time.Unix(int64(exp), 0), }, nil }