gotify/server · error · errCannotParseToken

%w: token must have 2 or 4 fields separated by dots

Error message

%w: token must have 2 or 4 fields separated by dots

What it means

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".

Source

Thrown at auth/token.go:117

	var seed [ed25519.SeedSize]byte
	_, err := rand.Read(seed[:])
	if err != nil {
		panic("unreachable: random source should never return an error")
	}
	return &EnhancedToken{ident: ident, pubOrPrivKey: seed[:]}
}

// ParseEnhancedToken parses a string into an EnhancedToken.
func ParseEnhancedToken(token string) (*EnhancedToken, error) {
	token, found := strings.CutPrefix(token, enhancedTokenPrefix)
	if !found {
		return nil, fmt.Errorf("%w: token must start with %s", errCannotParseToken, enhancedTokenPrefix)
	}

	// count number of dots, one dot -> ident then private key, three dots -> ident, public key, challenge then signature
	fields := strings.SplitN(token, ".", 4)
	if len(fields) != 2 && len(fields) != 4 {
		return nil, fmt.Errorf("%w: token must have 2 or 4 fields separated by dots", errCannotParseToken)
	}
	ident := fields[0]
	pkOrPubkeyB64 := fields[1]
	pkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))
	pkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)
	if err != nil {
		return nil, fmt.Errorf("%w: base64 decode failed: %w", errCannotParseToken, err)
	}
	if len(fields) == 2 {
		if pkOrPubkeyBytesLen != ed25519.SeedSize {
			return nil, fmt.Errorf("%w: private key must be %d bytes", errCannotParseToken, ed25519.SeedSize)
		}
		return &EnhancedToken{
			ident:        ident,
			pubOrPrivKey: pkOrPubkey,
		}, nil
	}
	if pkOrPubkeyBytesLen != ed25519.PublicKeySize {

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Use a token produced by EnhancedToken.String()/PublicForm() rather than hand-built strings.
  2. Verify the token body has exactly one dot (private-key form) or three dots (public-key/challenge/signature form).
  3. If you hold a JWT, parse it with the JWT verifier instead of ParseEnhancedToken.
  4. Regenerate the token and copy it in full, unmodified.

Example fix

// before: jwt-like input
auth.ParseEnhancedToken("gtfya.b.c.d.e")
// after: correct 4-field form
auth.ParseEnhancedToken("gtfy<ident>.<pubkeyB64>.<challenge>.<signature>")
Defensive patterns

Strategy: validation

Validate before calling

body := strings.TrimPrefix(tok, "gtfy")
n := strings.Count(body, ".")
if n != 1 && n != 3 {
    return errors.New("enhanced token must have 2 or 4 dot-separated fields")
}

Type guard

func wellFormedEnhancedToken(s string) bool {
    if !strings.HasPrefix(s, "gtfy") { return false }
    n := strings.Count(strings.TrimPrefix(s, "gtfy"), ".")
    return n == 1 || n == 3
}

Try / catch

if _, err := auth.ParseEnhancedToken(raw); errors.Is(err, auth.errCannotParseToken) {
    return fmt.Errorf("malformed token: %w", err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/68e45ea089d45d7b. Report an issue: GitHub.