{"record":{"id":"68e45ea089d45d7b","repo":"gotify/server","slug":"w-token-must-have-2-or-4-fields-separated-by-dot","errorCode":null,"errorMessage":"%w: token must have 2 or 4 fields separated by dots","messagePattern":"%w: token must have 2 or 4 fields separated by dots","errorType":"exception","errorClass":"errCannotParseToken","httpStatus":null,"severity":"error","filePath":"auth/token.go","lineNumber":117,"sourceCode":"\tvar seed [ed25519.SeedSize]byte\n\t_, err := rand.Read(seed[:])\n\tif err != nil {\n\t\tpanic(\"unreachable: random source should never return an error\")\n\t}\n\treturn &EnhancedToken{ident: ident, pubOrPrivKey: seed[:]}\n}\n\n// ParseEnhancedToken parses a string into an EnhancedToken.\nfunc ParseEnhancedToken(token string) (*EnhancedToken, error) {\n\ttoken, found := strings.CutPrefix(token, enhancedTokenPrefix)\n\tif !found {\n\t\treturn nil, fmt.Errorf(\"%w: token must start with %s\", errCannotParseToken, enhancedTokenPrefix)\n\t}\n\n\t// count number of dots, one dot -> ident then private key, three dots -> ident, public key, challenge then signature\n\tfields := strings.SplitN(token, \".\", 4)\n\tif len(fields) != 2 && len(fields) != 4 {\n\t\treturn nil, fmt.Errorf(\"%w: token must have 2 or 4 fields separated by dots\", errCannotParseToken)\n\t}\n\tident := fields[0]\n\tpkOrPubkeyB64 := fields[1]\n\tpkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))\n\tpkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: base64 decode failed: %w\", errCannotParseToken, err)\n\t}\n\tif len(fields) == 2 {\n\t\tif pkOrPubkeyBytesLen != ed25519.SeedSize {\n\t\t\treturn nil, fmt.Errorf(\"%w: private key must be %d bytes\", errCannotParseToken, ed25519.SeedSize)\n\t\t}\n\t\treturn &EnhancedToken{\n\t\t\tident:        ident,\n\t\t\tpubOrPrivKey: pkOrPubkey,\n\t\t}, nil\n\t}\n\tif pkOrPubkeyBytesLen != ed25519.PublicKeySize {","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/auth/token.go#L99-L135","documentation":"After stripping the \"gtfy\" prefix, ParseEnhancedToken splits the remainder on dots expecting either 2 fields (ident + private key) or 4 fields (ident + public key + challenge + signature). Any other dot count returns errCannotParseToken wrapped with \"token must have 2 or 4 fields separated by dots\".","triggerScenarios":"Calling ParseEnhancedToken with a prefixed string whose body has 1, 3, or 5+ dot-separated segments — e.g. a JWT (5 segments), a malformed manual concatenation, or a partially truncated token.","commonSituations":"Passing a JWT or other bearer token by mistake; manual token construction with wrong segment count; token corrupted by a newline/URL rewrite stripping a segment.","solutions":["Use a token produced by EnhancedToken.String()/PublicForm() rather than hand-built strings.","Verify the token body has exactly one dot (private-key form) or three dots (public-key/challenge/signature form).","If you hold a JWT, parse it with the JWT verifier instead of ParseEnhancedToken.","Regenerate the token and copy it in full, unmodified."],"exampleFix":"// before: jwt-like input\nauth.ParseEnhancedToken(\"gtfya.b.c.d.e\")\n// after: correct 4-field form\nauth.ParseEnhancedToken(\"gtfy<ident>.<pubkeyB64>.<challenge>.<signature>\")","handlingStrategy":"validation","validationCode":"body := strings.TrimPrefix(tok, \"gtfy\")\nn := strings.Count(body, \".\")\nif n != 1 && n != 3 {\n    return errors.New(\"enhanced token must have 2 or 4 dot-separated fields\")\n}","typeGuard":"func wellFormedEnhancedToken(s string) bool {\n    if !strings.HasPrefix(s, \"gtfy\") { return false }\n    n := strings.Count(strings.TrimPrefix(s, \"gtfy\"), \".\")\n    return n == 1 || n == 3\n}","tryCatchPattern":"if _, err := auth.ParseEnhancedToken(raw); errors.Is(err, auth.errCannotParseToken) {\n    return fmt.Errorf(\"malformed token: %w\", err)\n}","preventionTips":["Use EnhancedToken.String()/PublicForm() to serialize tokens; never hand-build them.","Don't pass JWTs or other bearer formats to ParseEnhancedToken.","Copy tokens in full — dropped segments change the dot count."],"tags":["auth","token","parsing"],"backgroundTag":"invalid-token-format","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}