{"record":{"id":"316eecff58d0fd39","repo":"grpc/grpc-go","slug":"expected-3-parts-in-token","errorCode":null,"errorMessage":"expected 3 parts in token","messagePattern":"expected 3 parts in token","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"credentials/jwt/file_reader.go","lineNumber":94,"sourceCode":"\tif !ok { // no period found\n\t\treturn \"\", false\n\t}\n\tclaims, s, ok := strings.Cut(s, tokenDelim)\n\tif !ok { // only one period found\n\t\treturn \"\", false\n\t}\n\t_, _, ok = strings.Cut(s, tokenDelim)\n\tif ok { // three periods found\n\t\treturn \"\", false\n\t}\n\treturn claims, true\n}\n\n// extractExpiration parses the JWT token to extract the expiration time.\nfunc (r *jwtFileReader) extractExpiration(token string) (time.Time, error) {\n\tclaimsRaw, ok := extractClaimsRaw(token)\n\tif !ok {\n\t\treturn time.Time{}, fmt.Errorf(\"expected 3 parts in token\")\n\t}\n\tpayloadBytes, err := base64.RawURLEncoding.DecodeString(claimsRaw)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"decode error: %v\", err)\n\t}\n\n\tvar claims jwtClaims\n\tif err := json.Unmarshal(payloadBytes, &claims); err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal error: %v\", err)\n\t}\n\n\tif claims.Exp == 0 {\n\t\treturn time.Time{}, fmt.Errorf(\"no expiration claims\")\n\t}\n\n\texpTime := time.Unix(claims.Exp, 0)\n\n\t// Check if token is already expired.","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/credentials/jwt/file_reader.go#L76-L112","documentation":"extractClaimsRaw returns ok=false when the token does not have exactly two '.' delimiters producing three non-empty segments (file_reader.go:74-95). extractExpiration then returns 'expected 3 parts in token'. The reader requires a well-formed JWS compact serialization even though it only reads the payload.","triggerScenarios":"The token string lacks dots, has only one dot, or has more than two dots. Caused by a non-JWT string, a JWT split across lines, trailing/leading whitespace inside the segments, or a token that was URL-encoded.","commonSituations":"Wrong file (raw key, refresh token, base64 blob), a token that was base64-encoded as a whole before being written, or whitespace/newlines in the middle of the file that TrimSpace at line 55 does not remove (it only trims ends).","solutions":["Confirm the file content is a single-line compact JWT: header.payload.signature.","Strip any internal whitespace/newlines before writing, not just leading/trailing.","Point the reader at the actual ID token file, not an OAuth/refresh token or key file.","Decode the string with a JWT tool to confirm the three-part structure."],"exampleFix":"// before: stored a multi-line PEM-ish blob\n// -----BEGIN TOKEN-----\n// ey...\n// -----END TOKEN-----\n\n// after: store the compact serialization on one line\neyJhbGciOi...<payload>...<sig>","handlingStrategy":"validation","validationCode":"// Check the three-part structure before use.\nparts := strings.Split(strings.TrimSpace(tok), \".\")\nif len(parts) != 3 {\n    return fmt.Errorf(\"token is not a 3-part JWT\")\n}","typeGuard":null,"tryCatchPattern":"_, _, err := r.readToken()\nif err != nil && strings.Contains(err.Error(), \"expected 3 parts in token\") {\n    // token is not a compact JWT; replace the file contents.\n    return err\n}","preventionTips":["Store the token as a single compact header.payload.signature line.","Strip internal whitespace/newlines, not just leading/trailing.","Use a real ID-token source rather than hand-edited strings.","Validate the structure with a JWT tool before deploying."],"tags":["grpc","jwt","validation","parsing","credentials"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}