{"record":{"id":"e95fe6c20b6aaf64","repo":"Billionmail/BillionMail","slug":"jwt-missing-group-token-claim","errorCode":null,"errorMessage":"JWT missing group_token claim","messagePattern":"JWT missing group_token claim","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/batch_mail/jwt.go","lineNumber":294,"sourceCode":"\t\tif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unexpected signing method: %v\", token.Header[\"alg\"])\n\t\t}\n\t\treturn []byte(cfg.secret), nil\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse JWT: %w\", err)\n\t}\n\tif claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {\n\t\tresult := &SubscribeConfirmClaims{}\n\t\tif email, ok := claims[\"email\"].(string); ok {\n\t\t\tresult.Email = email\n\t\t} else {\n\t\t\treturn nil, errors.New(\"JWT missing email claim\")\n\t\t}\n\t\tif groupToken, ok := claims[\"group_token\"].(string); ok {\n\t\t\tresult.GroupToken = groupToken\n\t\t} else {\n\t\t\treturn nil, errors.New(\"JWT missing group_token claim\")\n\t\t}\n\t\tif exp, ok := claims[\"exp\"].(float64); ok {\n\t\t\tif time.Now().Unix() > int64(exp) {\n\t\t\t\treturn nil, errors.New(\"JWT has expired\")\n\t\t\t}\n\t\t\tresult.RegisteredClaims.ExpiresAt = jwt.NewNumericDate(time.Unix(int64(exp), 0))\n\t\t}\n\t\treturn result, nil\n\t}\n\treturn nil, errors.New(\"invalid token claims\")\n}\n","sourceCodeStart":276,"sourceCodeEnd":306,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/batch_mail/jwt.go#L276-L306","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: signing omits claim\nclaims := jwt.MapClaims{\"email\": email, \"exp\": time.Now().Add(time.Hour).Unix()}\n// after\nclaims := jwt.MapClaims{\"email\": email, \"group_token\": groupToken, \"exp\": time.Now().Add(time.Hour).Unix()}","handlingStrategy":"type-guard","validationCode":"// Go: decode payload without trusting it first\ntok, _, err := jwt.NewParser().ParseUnverified(rawToken, jwt.MapClaims{})\nif err != nil { return err }\nif _, ok := tok.Claims.(jwt.MapClaims)[\"group_token\"]; !ok {\n    return errors.New(\"token lacks group_token claim; regenerate\")\n}","typeGuard":"func hasGroupToken(claims jwt.MapClaims) bool {\n    v, ok := claims[\"group_token\"]\n    return ok && typeof v == string && v != \"\"  // v.(string) with ok check\n}","tryCatchPattern":"result, err := ParseSubscribeConfirmJWT(raw)\nif err != nil {\n    if err.Error() == \"JWT missing group_token claim\" {\n        // re-issue token or treat as invalid link\n        return nil, fmt.Errorf(\"invalid confirmation link: %w\", err)\n    }\n    return nil, err\n}","preventionTips":["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."],"tags":["jwt","validation","batch-mail"],"backgroundTag":"jwt-missing-claim","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}