Billionmail/BillionMail · error
JWT missing group_token claim
Error message
JWT missing group_token claim
What it means
ParseSubscribeConfirmJWT parses a subscribe-confirmation JWT and requires an explicit "group_token" claim. When the token's claims map contains no group_token key (or it is not a string), parsing fails and this error is returned instead of a result. It guards downstream code that relies on GroupToken to identify the contact group being subscribed to.
Source
Thrown at core/internal/service/batch_mail/jwt.go:294
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(cfg.secret), nil
})
if err != nil {
return nil, fmt.Errorf("failed to parse JWT: %w", err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
result := &SubscribeConfirmClaims{}
if email, ok := claims["email"].(string); ok {
result.Email = email
} else {
return nil, errors.New("JWT missing email claim")
}
if groupToken, ok := claims["group_token"].(string); ok {
result.GroupToken = groupToken
} else {
return nil, errors.New("JWT missing group_token claim")
}
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, errors.New("JWT has expired")
}
result.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(time.Unix(int64(exp), 0))
}
return result, nil
}
return nil, errors.New("invalid token claims")
}
View on GitHub (pinned to fc36c76c05)
Solutions
- Regenerate the token with the group_token claim included as a string when creating the subscribe-confirmation JWT.
- Verify the same jwt secret and claim-setting code path is used by whatever signed the token (no stale or alternate generator).
- If group_token is genuinely optional for a flow, change the parse to treat it as optional instead of returning an error.
Example fix
// before: signing omits claim
claims := jwt.MapClaims{"email": email, "exp": time.Now().Add(time.Hour).Unix()}
// after
claims := jwt.MapClaims{"email": email, "group_token": groupToken, "exp": time.Now().Add(time.Hour).Unix()} Defensive patterns
Strategy: type-guard
Validate before calling
// Go: decode payload without trusting it first
tok, _, err := jwt.NewParser().ParseUnverified(rawToken, jwt.MapClaims{})
if err != nil { return err }
if _, ok := tok.Claims.(jwt.MapClaims)["group_token"]; !ok {
return errors.New("token lacks group_token claim; regenerate")
} Type guard
func hasGroupToken(claims jwt.MapClaims) bool {
v, ok := claims["group_token"]
return ok && typeof v == string && v != "" // v.(string) with ok check
} Try / catch
result, err := ParseSubscribeConfirmJWT(raw)
if err != nil {
if err.Error() == "JWT missing group_token claim" {
// re-issue token or treat as invalid link
return nil, fmt.Errorf("invalid confirmation link: %w", err)
}
return nil, err
} Prevention
- Always set group_token alongside email when minting subscribe-confirm tokens.
- Add a round-trip unit test: sign then parse, asserting GroupToken survives.
- Share a single token-builder helper so claim sets never drift between call sites.
When it happens
Trigger: Calling getEmailFromToken (which delegates to ParseSubscribeConfirmJWT) with a token signed without the group_token claim, or with group_token stored as a non-string JSON value (e.g. a number or object).
Common situations: Tokens minted by an older version of the token generator before group_token was added; a second signing service or script that only sets the email claim; hand-rolled tokens created for tests that omit optional-looking claims.
Related errors
- empty token string
- end_time must greater than start_time
- threads must be greater than zero
- threads must be less than 100
- task %d not found
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/e95fe6c20b6aaf64.
Report an issue: GitHub.