toeverything/AFFiNE · error · OauthStateExpired
oauth_state_expired
oauth_state_expired
Error message
OAuth state expired, please try again.
What it means
OauthStateExpired thrown at packages/backend/server/src/plugins/oauth/service.ts:100 when the 36-char state UUID is well-formed but not found in the challenge store (AuthChallengeStore over SessionCache with a 3-hour TTL, see OAUTH_STATE_TTL_MS). The state was never issued, has already expired, was consumed, or lives on another server node's cache.
Source
Thrown at packages/backend/server/src/plugins/oauth/service.ts:100
}): Promise<VerifyCallbackResult> {
let stateStr = input.stateStr;
let rawState: { state: string; provider?: string } | null = null;
if (typeof stateStr === 'string' && stateStr.length > 36) {
try {
const parsed = OAuthStateEnvelopeSchema.safeParse(JSON.parse(stateStr));
if (parsed.success) {
rawState = parsed.data;
stateStr = rawState.state;
}
} catch {} // noop
}
if (typeof stateStr !== 'string' || !this.isValidState(stateStr)) {
throw new InvalidOauthCallbackState();
}
const state = await this.getOAuthState(stateStr);
if (!state) throw new OauthStateExpired();
if (!state.token) state.token = stateStr;
if (
state.provider === OAuthProviderName.Apple &&
rawState &&
state.client &&
state.client !== 'web'
) {
return {
type: 'handoff',
code: input.code,
provider: rawState.provider,
state,
stateToken: stateStr,
};
}
if (!state.provider) {View on GitHub (pinned to b4c8548c09)
Solutions
- Have the user restart the sign-in flow — a fresh preflight issues a new state.
- For multi-instance deployments, point all nodes at the same shared Redis session cache so states are visible cluster-wide.
- If states expire too fast in your environment, ensure the cache backend is persistent (Redis AOF/RDB) rather than in-memory.
- Check for clock/TTL misconfiguration or Redis maxmemory eviction policy that is evicting oauth_state keys.
Defensive patterns
Strategy: retry
Try / catch
try {
await oauth.verifyCallback(input);
} catch (err) {
if (err instanceof OauthStateExpired) {
// user-facing: 'session expired, please sign in again' -> re-run preflight
}
} Prevention
- Share one Redis session cache across all backend replicas.
- Use a persistent Redis (AOF/RDB) so restarts do not wipe oauth_state keys.
- Complete login promptly; states live 3 hours.
- Avoid Redis eviction policies that evict auth_challenge:* keys under memory pressure.
When it happens
Trigger: Completing the OAuth callback more than 3 hours after preflight; server restart that wiped an in-memory/Redis cache without persistence; multi-instance deployment where the callback hits a node that does not share the session cache; the user's browser replaying an old callback; a state UUID that was fabricated (valid format, never saved).
Common situations: Self-hoster running several backend replicas behind a load balancer with cache pointing at different Redis DBs or memory cache; Redis flushed/evicted; user left the login tab open overnight; Docker restart with AFFINE default in-memory cache in production.
Related errors
- invalid_oauth_callback_state
- invalid_auth_state
- user_not_found
- authentication_required
- authentication_required
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/9086412e5b5f9352.
Report an issue: GitHub.