Billionmail/BillionMail · error
JWT missing or invalid group_id claim
Error message
JWT missing or invalid group_id claim
What it means
The 'group_id' claim is mandatory for unsubscribe tokens: it must be present and a JSON number (MapClaims decodes numbers as float64). Missing or wrong type aborts parsing. Note the asymmetry with template_id, which is optional.
Source
Thrown at core/internal/service/batch_mail/jwt.go:195
// Extract task ID
if taskID, ok := claims["task_id"].(float64); ok {
result.TaskId = int(taskID)
}
// Extract expiration (optional)
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()View on GitHub (pinned to fc36c76c05)
Solutions
- Ensure the issuer includes claims["group_id"] as a number (int in Go encodes as JSON number)
- Convert string group IDs to int before signing: gid, _ := strconv.Atoi(s)
- Verify the caller is using ParseSubscribeConfirmJWT/ParseUnsubscribeJWT matched to the token's producer flow
Example fix
// before claims["group_id"] = strconv.Itoa(groupID) // after claims["group_id"] = groupID
Defensive patterns
Strategy: validation
Validate before calling
// issuer-side guard before signing
if groupID <= 0 {
return fmt.Errorf("cannot sign unsubscribe token without group_id")
} Type guard
func claimInt(claims jwt.MapClaims, key string) (int, bool) {
f, ok := claims[key].(float64) // encoding/json numbers decode as float64
return int(f), ok
} Try / catch
claims, err := ParseUnsubscribeJWT(tok)
if err != nil {
if strings.Contains(err.Error(), "group_id") {
return nil, ErrBadTokenSchema
}
return err
} Prevention
- Never stringify numeric claims (group_id must be JSON number)
- Round-trip test generator→parser for every claim field
- Match parser to producer flow: unsubscribe claims vs subscribe-confirm claims
- Pin a schema doc/constants file for claim keys
When it happens
Trigger: Token signed without group_id, with group_id as a string (e.g. "12"), or with a null value; tokens from a different claim schema hitting ParseUnsubscribeJWT.
Common situations: Custom/legacy token generators encoding group_id as a string; a schema change on the issuing side; tokens minted by the subscribe-confirm flow (which uses group_token, not group_id) parsed by the wrong function if secrets coincide.
Related errors
- JWT missing email claim
- invalid token claims
- unexpected signing method: %v
- failed to parse JWT: %w
- JWT has expired
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/b5c2fe6eaa8460ec.
Report an issue: GitHub.