Billionmail/BillionMail · error

invalid token claims

Error message

invalid token claims

What it means

Fallback error: jwt.Parse succeeded but the top-level type assertion token.Claims.(jwt.MapClaims) failed or token.Valid is false, so the structured claim-extraction branch never ran. Rare in practice — jwt.Parse with no explicit claims type yields MapClaims, so this mostly fires on parser misconfiguration or token.Valid being false despite nil error.

Source

Thrown at core/internal/service/batch_mail/jwt.go:202

		if exp, ok := claims["exp"].(float64); ok {
			// Check if token has expired
			if time.Now().Unix() > int64(exp) {
				return nil, errors.New("JWT has expired")
			}
			result.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(time.Unix(int64(exp), 0))
		}
		// Extract group ID
		if groupID, ok := claims["group_id"].(float64); ok {
			result.GroupId = int(groupID)
		} else {
			return nil, errors.New("JWT missing or invalid group_id claim")
		}

		g.Log().Debug(context.Background(), "JWT parsed successfully: %+v", result)
		return result, nil
	}

	return nil, errors.New("invalid token claims")
}

type SubscribeConfirmClaims struct {
	Email      string `json:"email"`
	GroupToken string `json:"group_token"`
	jwt.RegisteredClaims
}

func getSubscribeConfirmConfig() *jwtConfig {
	once.Do(func() {
		config = loadSubscribeConfirmConfig()
	})
	return config
}

func loadSubscribeConfirmConfig() *jwtConfig {
	ctx := gctx.New()
	return &jwtConfig{

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Keep using plain jwt.Parse (default MapClaims) or change the assertion to match the configured claims type
  2. Log token.Valid and the concrete claims type when debugging
  3. If migrating to typed claims, port the email/group_id extraction into the struct's UnmarshalJSON

Example fix

// before
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
// after
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
Defensive patterns

Strategy: type-guard

Validate before calling

// ensure default MapClaims path: use jwt.Parse, not a custom-claims parser
token, err := jwt.Parse(tokenString, keyfunc) // claims type stays jwt.MapClaims

Type guard

func validMapClaims(token *jwt.Token) (jwt.MapClaims, bool) {
	c, ok := token.Claims.(jwt.MapClaims)
	return c, ok && token.Valid
}

Try / catch

claims, err := ParseUnsubscribeJWT(tok)
if err != nil {
	if strings.Contains(err.Error(), "invalid token claims") {
		log.Printf("claims assertion failed: token=%T valid=%v", nil, false)
	}
	return err
}

Prevention

When it happens

Trigger: A jwt.Parser configured with a different claims type (e.g. custom Claims struct) instead of default MapClaims; token.Valid false from a non-standard validation setup; future refactors changing the Parse call.

Common situations: Refactoring that swaps jwt.Parse for jwt.NewParser with ValidityChecks or a typed claims target; library-version migrations (golang-jwt v4/v5 differences in Valid semantics).

Understand the failure class

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/9d953b199c447ee1. Report an issue: GitHub.