{"record":{"id":"9d953b199c447ee1","repo":"Billionmail/BillionMail","slug":"invalid-token-claims","errorCode":null,"errorMessage":"invalid token claims","messagePattern":"invalid token claims","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/batch_mail/jwt.go","lineNumber":202,"sourceCode":"\t\tif exp, ok := claims[\"exp\"].(float64); ok {\n\t\t\t// Check if token has expired\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\t// Extract group ID\n\t\tif groupID, ok := claims[\"group_id\"].(float64); ok {\n\t\t\tresult.GroupId = int(groupID)\n\t\t} else {\n\t\t\treturn nil, errors.New(\"JWT missing or invalid group_id claim\")\n\t\t}\n\n\t\tg.Log().Debug(context.Background(), \"JWT parsed successfully: %+v\", result)\n\t\treturn result, nil\n\t}\n\n\treturn nil, errors.New(\"invalid token claims\")\n}\n\ntype SubscribeConfirmClaims struct {\n\tEmail      string `json:\"email\"`\n\tGroupToken string `json:\"group_token\"`\n\tjwt.RegisteredClaims\n}\n\nfunc getSubscribeConfirmConfig() *jwtConfig {\n\tonce.Do(func() {\n\t\tconfig = loadSubscribeConfirmConfig()\n\t})\n\treturn config\n}\n\nfunc loadSubscribeConfirmConfig() *jwtConfig {\n\tctx := gctx.New()\n\treturn &jwtConfig{","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/batch_mail/jwt.go#L184-L220","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Keep using plain jwt.Parse (default MapClaims) or change the assertion to match the configured claims type","Log token.Valid and the concrete claims type when debugging","If migrating to typed claims, port the email/group_id extraction into the struct's UnmarshalJSON"],"exampleFix":"// before\nif claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {\n// after\nif claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {","handlingStrategy":"type-guard","validationCode":"// ensure default MapClaims path: use jwt.Parse, not a custom-claims parser\ntoken, err := jwt.Parse(tokenString, keyfunc) // claims type stays jwt.MapClaims","typeGuard":"func validMapClaims(token *jwt.Token) (jwt.MapClaims, bool) {\n\tc, ok := token.Claims.(jwt.MapClaims)\n\treturn c, ok && token.Valid\n}","tryCatchPattern":"claims, err := ParseUnsubscribeJWT(tok)\nif err != nil {\n\tif strings.Contains(err.Error(), \"invalid token claims\") {\n\t\tlog.Printf(\"claims assertion failed: token=%T valid=%v\", nil, false)\n\t}\n\treturn err\n}","preventionTips":["Do not swap jwt.Parse for jwt.NewParser with a typed claims target without updating the assertion","Add a regression test for the full happy-path parse","Keep token.Valid checked together with the claims type assertion","Review golang-jwt migration notes (v4→v5) when upgrading"],"tags":["jwt","go","claims","auth"],"backgroundTag":"jwt-invalid-claims","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"}