{"record":{"id":"d48d20b953d25d00","repo":"gotify/server","slug":"w-token-must-start-with-s","errorCode":null,"errorMessage":"%w: token must start with %s","messagePattern":"%w: token must start with (.+?)","errorType":"exception","errorClass":"errCannotParseToken","httpStatus":null,"severity":"error","filePath":"auth/token.go","lineNumber":111,"sourceCode":"\treturn b.String()\n}\n\n// NewEnhancedToken creates a new EnhancedToken.\nfunc NewEnhancedToken(ident string) *EnhancedToken {\n\tident = strings.ReplaceAll(ident, \".\", \"_\")\n\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}","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/auth/token.go#L93-L129","documentation":"ParseEnhancedToken only accepts tokens prefixed with the enhanced-token prefix \"gtfy\". If the string lacks that prefix it returns an error wrapping errCannotParseToken with \"token must start with gtfy\". This guards against passing plain IDs, other token formats, or truncated tokens into the enhanced-token parser.","triggerScenarios":"Calling ParseEnhancedToken (directly or via client/application APIs such as CreateClient or CreateApplication that accept a token parameter) with a string not beginning with \"gtfy\" — e.g. a raw OAuth token, a copied identifier, or the token stripped of its prefix.","commonSituations":"Copy-pasting only the body after the prefix; storing tokens without the prefix in env vars; confusing enhanced tokens with plain access tokens from an older version.","solutions":["Pass the full token including the \"gtfy\" prefix exactly as returned by PublicForm/String().","Check where the token is stored/generated — do not trim or transform it before parsing.","If you have a non-enhanced token, use the appropriate parser for that format instead.","Regenerate the token via the API so you receive a properly prefixed enhanced token."],"exampleFix":"// before\ntok, err := auth.ParseEnhancedToken(\"abc.def\")\n// after\ntok, err := auth.ParseEnhancedToken(\"gtfyabc.def\")","handlingStrategy":"validation","validationCode":"func hasEnhancedTokenPrefix(tok string) bool {\n    return strings.HasPrefix(tok, \"gtfy\")\n}","typeGuard":"func isEnhancedToken(s string) bool {\n    return strings.HasPrefix(s, \"gtfy\") && strings.Count(strings.TrimPrefix(s, \"gtfy\"), \".\")%2 == 1\n}","tryCatchPattern":"tok, err := auth.ParseEnhancedToken(raw)\nif errors.Is(err, auth.errCannotParseToken) {\n    // (exported via wrapping) treat as malformed token: re-authenticate or regenerate\n}","preventionTips":["Store and pass tokens exactly as issued, including the gtfy prefix.","Never trim, URL-encode, or wrap tokens in transport/storage.","Distinguish enhanced tokens from other token types at the call site."],"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"}