Tencent/WeKnora · error
oauth state not found or expired
Error message
oauth state not found or expired
What it means
Take retrieves and deletes the OAuthState (single-use CSRF state) during the callback. In Redis-backed mode a missing key (redis.Nil) returns this error. Because Take is get-and-delete, this error also fires on any second use of the same state.
Source
Thrown at internal/mcp/oauth_state.go:192
}
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.attempts[state]
if !ok || time.Now().After(entry.expiresAt) {
delete(s.attempts, state)
return OAuthAttempt{}, fmt.Errorf("oauth attempt not found or expired")
}
return entry.value, nil
}
// Take retrieves and deletes a state (single-use). Returns an error if the
// state is unknown or expired.
func (s *oauthStateStore) Take(ctx context.Context, state string) (OAuthState, error) {
if s.rdb != nil {
data, err := s.rdb.GetDel(ctx, s.key(state)).Bytes()
if err != nil {
if err == redis.Nil {
return OAuthState{}, fmt.Errorf("oauth state not found or expired")
}
return OAuthState{}, err
}
var v OAuthState
if err := json.Unmarshal(data, &v); err != nil {
return OAuthState{}, err
}
return v, nil
}
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.mem[state]
if !ok {
return OAuthState{}, fmt.Errorf("oauth state not found or expired")
}
delete(s.mem, state)
if time.Now().After(entry.expiresAt) {
return OAuthState{}, fmt.Errorf("oauth state not found or expired")View on GitHub (pinned to 988cbb0330)
Solutions
- Since Take is single-use, ensure the callback handler runs only once and handles replayed callbacks gracefully (check whether the token was already stored).
- Restart the OAuth flow with a fresh state if the first attempt genuinely expired.
- Enable Redis persistence / verify the same Redis is used by the authorize-start and callback endpoints.
- Reject duplicate callbacks with a friendly 'authorization already completed' response.
Example fix
// before
state, err := store.Take(ctx, r.URL.Query().Get("state"))
if err != nil { return err }
// after
st, err := store.Take(ctx, stateParam)
if err != nil {
if tokenStored(ctx, session) { return redirectDone() } // replay after success
return restartOAuthFlow()
} Defensive patterns
Strategy: try-catch
Validate before calling
// check for prior completion before Take:
if _, err := tokenStore.GetToken(ctx, principal); err == nil { // already completed; ignore duplicate callback } Try / catch
st, err := store.Take(ctx, stateParam)
if err != nil {
if strings.Contains(err.Error(), "not found or expired") {
// single-use replay or expiry: check token, else restart flow
if done := maybeAlreadyCompleted(ctx, session); done { return redirectDone() }
return restartAuthorization()
}
return err
} Prevention
- Never call Take more than once per callback; guard the handler against browser retries.
- Enable Redis persistence and share the instance across callback handlers.
- Issue fresh state per attempt; never reuse or pre-generate states.
- Treat unknown states as potential CSRF and log them with request metadata.
When it happens
Trigger: CompleteAuthorization calls Take with a state whose Redis key is gone — expired TTL, Redis restart/flush, replayed callback (state already consumed by a prior Take), or state never issued by this deployment.
Common situations: Double callback delivery (browser retry, webhook replay); user clicked the authorization link twice and both callbacks fired; Redis persistence disabled and the instance restarted mid-flow; stale bookmarked callback URL reused later.
Related errors
- oauth attempt not found or expired
- WEKNORA_REDIS_NAMESPACE must not contain braces
- WEKNORA_REDIS_NAMESPACE must not contain control characters
- failed to get FAQ import progress from Redis: %w
- failed to unmarshal FAQ import progress: %w
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/ef97fbe755158e93.
Report an issue: GitHub.