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

  1. Regenerate the token with the group_token claim included as a string when creating the subscribe-confirmation JWT.
  2. Verify the same jwt secret and claim-setting code path is used by whatever signed the token (no stale or alternate generator).
  3. 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

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


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