{"record":{"id":"b37422abb63e6a1b","repo":"crowdsecurity/crowdsec","slug":"errcookieexpired","errorCode":"ErrCookieExpired","errorMessage":"%w: not_after=%d now=%d","messagePattern":"%w: not_after=(.+?) now=(.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/appsec/challenge/crypto.go","lineNumber":259,"sourceCode":"\n\tif len(plaintext) < cookiePlaintextFixedHeaderLen {\n\t\treturn nil, fmt.Errorf(\"%w: plaintext shorter than fixed header\", ErrCookieMalformed)\n\t}\n\n\tnotAfter := int64(binary.BigEndian.Uint64(plaintext[:8]))\n\tflags := plaintext[8]\n\treasonLen := int(binary.BigEndian.Uint16(plaintext[9:11]))\n\n\tif reasonLen > MaxAllowlistReasonLen {\n\t\treturn nil, fmt.Errorf(\"%w: reason_len=%d\", ErrCookieMalformed, reasonLen)\n\t}\n\n\tif len(plaintext) < cookiePlaintextFixedHeaderLen+reasonLen {\n\t\treturn nil, fmt.Errorf(\"%w: plaintext shorter than declared reason_len\", ErrCookieMalformed)\n\t}\n\n\tif notAfter <= now.Unix() {\n\t\treturn nil, fmt.Errorf(\"%w: not_after=%d now=%d\", ErrCookieExpired, notAfter, now.Unix())\n\t}\n\n\treasonStart := cookiePlaintextFixedHeaderLen\n\treasonEnd := reasonStart + reasonLen\n\treason := string(plaintext[reasonStart:reasonEnd])\n\n\tenvelope := &pb.ChallengeCookie{}\n\tif err := proto.Unmarshal(plaintext[reasonEnd:], envelope); err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %w\", ErrCookiePayload, err)\n\t}\n\n\treturn &CookieEnvelope{\n\t\tEnvelope:        envelope,\n\t\tAllowlisted:     flags&cookieFlagAllowlisted != 0,\n\t\tAllowlistReason: reason,\n\t\tNotAfter:        notAfter,\n\t}, nil\n}","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/appsec/challenge/crypto.go#L241-L277","documentation":"openCookieV0Bytes rejects the cookie because its not_after timestamp is <= the current time, i.e. the challenge cookie has reached its expiration. It wraps ErrCookieExpired, which callers match with errors.Is to distinguish expiry from other malformed-cookie failures.","triggerScenarios":"openCookie is called after the cookie's not_after timestamp has passed — the cookie was issued more than its validity window ago, or the verifier's clock is ahead of the issuer's.","commonSituations":"A browser or API client caches the challenge cookie past its TTL and replays it; clock skew between distributed CrowdSec instances behind a load balancer; long-lived sessions resuming after keyring rotation; tests replaying a recorded cookie.","solutions":["Have the client obtain a fresh challenge cookie and retry","Synchronize clocks (NTP) across all instances sharing the master_secret","Increase the cookie validity window at issuance if legitimate lifetimes are being cut short","Treat ErrCookieExpired as a normal flow: clear the expired cookie client-side and restart the challenge"],"exampleFix":"// before: generic handling\nif err != nil { return err }\n// after\nif errors.Is(err, challenge.ErrCookieExpired) {\n    return issueNewChallenge() // fresh cookie, not an error for the user\n}","handlingStrategy":"try-catch","validationCode":"if errors.Is(err, challenge.ErrCookieExpired) { /* refresh */ }","typeGuard":"func isCookieExpired(err error) bool { return errors.Is(err, challenge.ErrCookieExpired) }","tryCatchPattern":"env, err := rt.OpenCookie(raw, now)\nif errors.Is(err, challenge.ErrCookieExpired) {\n    clearClientCookie(w)\n    return issueNewChallenge(w, r)\n} else if err != nil {\n    return err\n}","preventionTips":["Refresh cookies client-side before their TTL elapses","Run NTP on all instances sharing the master secret","Set cookie validity windows comfortably above expected session lengths","In tests, inject time instead of replaying recorded cookies"],"tags":["appsec","cookie","expiry"],"backgroundTag":"jwt-token-expired","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}