{"record":{"id":"3aa6eea3459a96b3","repo":"gotify/server","slug":"w-timestamp-must-be-an-integer-w","errorCode":null,"errorMessage":"%w: timestamp must be an integer: %w","messagePattern":"%w: timestamp must be an integer: %w","errorType":"exception","errorClass":"errCannotParseToken","httpStatus":null,"severity":"error","filePath":"auth/token.go","lineNumber":141,"sourceCode":"\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 {\n\t\treturn nil, fmt.Errorf(\"%w: public key must be %d bytes\", errCannotParseToken, ed25519.PublicKeySize)\n\t}\n\ttimestampStr := fields[2]\n\ttimestamp, err := strconv.ParseInt(timestampStr, 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: timestamp must be an integer: %w\", errCannotParseToken, err)\n\t}\n\tsignatureB64 := fields[3]\n\tsignatureBytesLen := base64.RawURLEncoding.DecodedLen(len(signatureB64))\n\tif signatureBytesLen != ed25519.SignatureSize {\n\t\treturn nil, fmt.Errorf(\"%w: signature must be %d bytes\", errCannotParseToken, ed25519.SignatureSize)\n\t}\n\tsignature, err := base64.RawURLEncoding.DecodeString(signatureB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: base64 decode failed: %w\", errCannotParseToken, err)\n\t}\n\tsha512 := sha512.New()\n\tsha512.Write([]byte(\"iat=\")) // query-like encoding to give us some semantic headroom should we need more fields in the future\n\tfmt.Fprintf(sha512, \"%d\", timestamp)\n\tif err := ed25519.VerifyWithOptions(pkOrPubkey, sha512.Sum(nil), signature, &ed25519.Options{Hash: crypto.SHA512}); err != nil {\n\t\treturn nil, errInvalidToken\n\t}\n\treturn &EnhancedToken{\n\t\tident:        ident,","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/auth/token.go#L123-L159","documentation":"ParseEnhancedToken parses a dot-separated enhanced token of the form pubkey.pk.timestamp.signature. Field 3 (index 2) is the issuance timestamp and must parse as a base-10 int64. If strconv.ParseInt fails, the token is rejected and the error is wrapped in errCannotParseToken so callers can errors.Is/As against it.","triggerScenarios":"Calling ParseEnhancedToken with a token whose third dot-separated field is not a valid decimal integer (empty field, non-numeric text, float like '1757000000.5', or a value overflowing int64 such as a 20+ digit number).","commonSituations":"Tokens truncated when copy-pasting, tokens hand-assembled or tampered with, mismatched token format after a library version change that added or reordered fields, or passing a base64/URL-encoded token where the encoding corrupted digits.","solutions":["Regenerate the token on the server with the current signing code so the timestamp field is a plain base-10 int64 (unix seconds).","Verify the token has exactly the expected number of dot-separated fields and that field index 2 is the timestamp; do not reorder fields.","Check the token was not truncated or mangled in transit (trailing whitespace stripped, no URL-encoding of dots).","Handle the wrapped errCannotParseToken in the caller and prompt the user to re-authenticate instead of retrying."],"exampleFix":"// before\ntoken := \"pk.pk. 1757000000.sig\" // timestamp field contains a space\nparsed, err := ParseEnhancedToken(token)\n// after\ntoken := \"pk.pk.1757000000.sig\" // field 2 is a clean int64\nparsed, err := ParseEnhancedToken(token)\nif errors.Is(err, errCannotParseToken) { /* re-authenticate */ }","handlingStrategy":"validation","validationCode":"parts := strings.Split(token, \".\")\nif len(parts) != 4 {\n    return fmt.Errorf(\"token must have 4 dot-separated fields, got %d\", len(parts))\n}\nif _, err := strconv.ParseInt(parts[2], 10, 64); err != nil {\n    return fmt.Errorf(\"timestamp field %q is not an int64\", parts[2])\n}","typeGuard":"func isParseableToken(token string) bool {\n    parts := strings.Split(token, \".\")\n    if len(parts) != 4 { return false }\n    _, err := strconv.ParseInt(parts[2], 10, 64)\n    return err == nil\n}","tryCatchPattern":"parsed, err := ParseEnhancedToken(token)\nif errors.Is(err, errCannotParseToken) {\n    // token malformed: force re-auth, do not retry\n    return nil, fmt.Errorf(\"malformed token: %w\", err)\n}\nif err != nil {\n    return nil, err // signature verification failed (errInvalidToken)\n}","preventionTips":["Generate tokens only via the library's signer so field order and formats stay consistent","Trim whitespace and reject tokens with unexpected length before parsing","Never hand-edit token fields; treat tokens as opaque strings","Pin the token format version and re-issue tokens when the format changes"],"tags":["go","token-parsing","ed25519","auth"],"backgroundTag":"token-parse-failed","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"}