gitroomhq/postiz-app · error · Error

Invalid state

Error message

Invalid state

What it means

OAuth CSRF protection: checkExists compares the `state` query parameter against the state cookie written when the OAuth flow started. If they mismatch (or state is missing) the request is rejected. The check is skipped when NOT_SECURED is set or when a redirectUri (native flow) is supplied.

Source

Thrown at apps/backend/src/services/auth/auth.service.ts:310

    const providerInstance = this._providerManager.getProvider(provider);
    return providerInstance.generateLink(query);
  }

  async checkExists(
    provider: string,
    code: string,
    redirectUri?: string,
    state?: string,
    stateCookie?: string
  ) {
    // the mobile app passes redirect_uri and keeps no cookies, the web flow
    // never passes it, so the state nonce is only enforced for the web flow
    if (
      !process.env.NOT_SECURED &&
      !redirectUri &&
      (!state || state !== stateCookie)
    ) {
      throw new Error('Invalid state');
    }

    const providerInstance = this._providerManager.getProvider(provider);
    const token = await providerInstance.getToken(code, redirectUri);
    const user = await providerInstance.getUser(token);
    if (!user) {
      throw new Error('Invalid user');
    }
    const checkExists = await this._userService.getUserByProvider(
      user.id,
      provider as Provider
    );
    if (checkExists) {
      return { jwt: await this.jwt(checkExists) };
    }

    return { token };
  }

View on GitHub (pinned to 0f1647f749)

Solutions

  1. Restart the OAuth flow from the beginning in the same browser session so a fresh state cookie is set
  2. Verify the frontend and backend share the domain/path expected by the state cookie (check cookie domain, SameSite, secure flags behind HTTPS)
  3. Do not manually copy/paste callback URLs between browsers or incognito windows
  4. For local testing only, set NOT_SECURED to bypass (never in production)
  5. If implementing a native flow, pass redirectUri so the web-only state nonce isn't enforced

Example fix

// before — starting flow in one context, finishing in another
window.location.href = authUrl; // cookie set here
// callback opened in a different browser -> 'Invalid state'

// after — ensure same browser session for both legs
const state = crypto.randomUUID();
document.cookie = `oauth_state=${state}; path=/; SameSite=Lax`;
window.location.href = `${authUrl}&state=${state}`;
Defensive patterns

Strategy: try-catch

Validate before calling

const stateCookie = getCookie('state');
if (!stateCookie) {
  restartOAuthFlow(); // no cookie -> callback will always fail
}

Type guard

null

Try / catch

try {
  await authService.checkExists({ code, provider, state });
} catch (e) {
  if (e instanceof Error && e.message === 'Invalid state') {
    // restart the flow from step 1; never retry the same callback URL
    return redirectToLogin();
  }
  throw e;
}

Prevention

When it happens

Trigger: Completing an OAuth callback where the `state` param was lost/changed, the state cookie is missing (cleared cookies, different domain, cross-browser), the flow started in one browser and finished in another, or the cookie expired before callback.

Common situations: Cookies blocked by browser settings or SameSite issues behind a proxy; opening the callback URL in a new browser/device; misconfigured frontend/backend domains so the cookie never reaches the callback request; native app flow passing the wrong arguments so redirectUri is empty.

Related errors


AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27). Data as JSON: /api/errors/4b9d7c05d57be20f. Report an issue: GitHub.