gotify/server · error · errCannotParseToken

%w: token must start with %s

Error message

%w: token must start with %s

What it means

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.

Source

Thrown at auth/token.go:111

	return b.String()
}

// NewEnhancedToken creates a new EnhancedToken.
func NewEnhancedToken(ident string) *EnhancedToken {
	ident = strings.ReplaceAll(ident, ".", "_")
	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)
		}

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Pass the full token including the "gtfy" prefix exactly as returned by PublicForm/String().
  2. Check where the token is stored/generated — do not trim or transform it before parsing.
  3. If you have a non-enhanced token, use the appropriate parser for that format instead.
  4. Regenerate the token via the API so you receive a properly prefixed enhanced token.

Example fix

// before
tok, err := auth.ParseEnhancedToken("abc.def")
// after
tok, err := auth.ParseEnhancedToken("gtfyabc.def")
Defensive patterns

Strategy: validation

Validate before calling

func hasEnhancedTokenPrefix(tok string) bool {
    return strings.HasPrefix(tok, "gtfy")
}

Type guard

func isEnhancedToken(s string) bool {
    return strings.HasPrefix(s, "gtfy") && strings.Count(strings.TrimPrefix(s, "gtfy"), ".")%2 == 1
}

Try / catch

tok, err := auth.ParseEnhancedToken(raw)
if errors.Is(err, auth.errCannotParseToken) {
    // (exported via wrapping) treat as malformed token: re-authenticate or regenerate
}

Prevention

When it happens

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

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

Related errors


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