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

  1. Since Take is single-use, ensure the callback handler runs only once and handles replayed callbacks gracefully (check whether the token was already stored).
  2. Restart the OAuth flow with a fresh state if the first attempt genuinely expired.
  3. Enable Redis persistence / verify the same Redis is used by the authorize-start and callback endpoints.
  4. 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

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


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/ef97fbe755158e93. Report an issue: GitHub.