crowdsecurity/crowdsec · error
unable to parse jwt expiration: %w
Error message
unable to parse jwt expiration: %w
What it means
After a successful watcher authentication, the server-issued expiration timestamp (authResp.Expire) is parsed with time.Time.UnmarshalText, which requires RFC3339 format. If the server returned an empty or malformed expiration string, parsing fails and InitLAPIClient aborts even though the token itself was obtained.
Source
Thrown at pkg/apiclient/client.go:134
PapiURL: papiURL,
VersionPrefix: "v1",
UpdateScenario: func(_ context.Context) ([]string, error) {
return scenarios, nil
},
})
authResp, _, err := client.Auth.AuthenticateWatcher(ctx, models.WatcherAuthRequest{
MachineID: &login,
Password: &pwd,
Scenarios: scenarios,
})
if err != nil {
return fmt.Errorf("authenticate watcher (%s): %w", login, err)
}
var expiration time.Time
if err := expiration.UnmarshalText([]byte(authResp.Expire)); err != nil {
return fmt.Errorf("unable to parse jwt expiration: %w", err)
}
client.GetClient().Transport.(*JWTTransport).Token = authResp.Token
client.GetClient().Transport.(*JWTTransport).Expiration = expiration
lapiClient = client
return nil
}
func GetLAPIClient() (*ApiClient, error) {
if lapiClient == nil {
return nil, errors.New("client not initialized")
}
return lapiClient, nil
}
View on GitHub (pinned to 909b515798)
Solutions
- Check what the LAPI actually returns for the auth endpoint (curl -X POST .../watchers/login) and inspect the 'expirations'/'expire' field format
- Align crowdsec client and LAPI server versions; a format change between versions can cause this
- If a proxy is involved, verify it passes the JSON body through unmodified
- Check that the response is real JSON from LAPI and not an HTML error page (a 200 with wrong body decodes into an empty Expire)
Defensive patterns
Strategy: validation
Validate before calling
// after a successful raw auth call, check the expiration field before parsing
if authResp.Expire == "" {
return errors.New("server returned empty jwt expiration")
}
if _, err := time.Parse(time.RFC3339, authResp.Expire); err != nil {
return fmt.Errorf("unexpected expiration format %q: %w", authResp.Expire, err)
} Type guard
func hasValidExpire(resp *models.WatcherAuthResponse) bool {
if resp == nil || resp.Expire == "" {
return false
}
_, err := time.Parse(time.RFC3339, resp.Expire)
return err == nil
} Prevention
- Keep crowdsec client and LAPI server versions aligned
- Ensure no proxy rewrites or truncates the auth JSON response
- Verify the auth endpoint returns application/json, not an HTML error page
When it happens
Trigger: expiration.UnmarshalText([]byte(authResp.Expire)) fails because authResp.Expire is empty, null, or not in RFC3339 format (e.g. '2026-09-06 12:00:00' without T/timezone, or a non-date string).
Common situations: LAPI behind a proxy that mangles the JSON response; version mismatch between client and server returning a different date format; auth endpoint hit through a wrong path returning HTML instead of the expected JSON; a mocked/test server returning incomplete WatcherAuthResponse.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- timestamp is not valid
- failed to extract claims
- MachineID not found in claims
- failed to cast machineID to string
- unable to generate a new random seed for JWT generation
AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06).
Data as JSON: /api/errors/29242b3584bc80fb.
Report an issue: GitHub.