{"record":{"id":"764f1565fb15fb39","repo":"vxcontrol/pentagi","slug":"token-is-malformed","errorCode":null,"errorMessage":"token is malformed","messagePattern":"token is malformed","errorType":"validation","errorClass":null,"httpStatus":401,"severity":"error","filePath":"backend/pkg/server/auth/api_token_jwt.go","lineNumber":49,"sourceCode":"\t\t\tExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(ttl) * time.Second)),\n\t\t\tIssuedAt:  jwt.NewNumericDate(now),\n\t\t\tSubject:   \"api_token\",\n\t\t},\n\t}\n}\n\nfunc ValidateAPIToken(tokenString, globalSalt string) (*models.APITokenClaims, error) {\n\tvar claims models.APITokenClaims\n\ttoken, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) {\n\t\t// verify signing algorithm to prevent \"alg: none\"\n\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 MakeJWTSigningKey(globalSalt), nil\n\t})\n\tif err != nil {\n\t\tif errors.Is(err, jwt.ErrTokenMalformed) {\n\t\t\treturn nil, fmt.Errorf(\"token is malformed\")\n\t\t} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {\n\t\t\treturn nil, fmt.Errorf(\"token is either expired or not active yet\")\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"token invalid: %w\", err)\n\t\t}\n\t}\n\n\tif !token.Valid {\n\t\treturn nil, fmt.Errorf(\"token is invalid\")\n\t}\n\n\treturn &claims, nil\n}\n","sourceCodeStart":31,"sourceCodeEnd":63,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/server/auth/api_token_jwt.go#L31-L63","documentation":"ValidateAPIToken parses an API-token JWT (HS256, keyed by MakeJWTSigningKey(globalSalt)). When jwt/v5 parsing fails with jwt.ErrTokenMalformed the function re-wraps it as \"token is malformed\", meaning the string itself is not a decodable JWT (bad compact JWS structure, invalid base64, unparsable JSON header/payload).","triggerScenarios":"Calling ValidateAPIToken (via bearer-token auth in tryProtoTokenAuthentication) with a string that is empty, truncated, has wrong number of dot-separated segments, contains non-base64 characters, or whose payload/header is not valid JSON.","commonSituations":"Client sends a session cookie or other non-JWT string in the Authorization header; token truncated by an HTTP proxy or copied with missing characters; client encodes token with different padding/URL-safe base64 variant; tests passing placeholder strings like \"test-token\".","solutions":["Fix the client to send the exact token string returned by the token-creation API (MakeAPIToken) in the Authorization: Bearer header","Log the received Authorization value (length/segment count) and compare against the stored token to spot truncation or corruption","Check for middleware/proxies that rewrite or trim the Authorization header","Ensure the client does not double-encode (e.g. base64 the token again before sending)"],"exampleFix":"// before\nreq.Header.Set(\"Authorization\", \"Bearer \" + base64.StdEncoding.EncodeToString([]byte(token)))\n// after\nreq.Header.Set(\"Authorization\", \"Bearer \" + token)","handlingStrategy":"validation","validationCode":"// before sending\nparts := strings.Split(token, \".\")\nif len(parts) != 3 || token == \"\" {\n    return fmt.Errorf(\"token must be a 3-segment JWT, got %d segments\", len(parts))\n}","typeGuard":"func isJWTShape(s string) bool {\n    parts := strings.Split(s, \".\")\n    return len(parts) == 3 && parts[0] != \"\" && parts[1] != \"\"\n}","tryCatchPattern":null,"preventionTips":["Send the token exactly as returned by the API; never re-encode it","Assert the Authorization header value equals \"Bearer \" + token before dispatching requests","Log token length/segment count (not the token) on auth failures to catch truncation","Check proxies/clients for header rewriting"],"tags":["jwt","authentication","malformed-token"],"backgroundTag":"jwt-malformed-token","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}