toeverything/AFFiNE · error · InvalidOauthResponse

invalid_oauth_response

invalid_oauth_response

Error message

Invalid OAuth response: Missing PKCE challenge for OIDC authorization request.

What it means

OIDCProvider.getAuthUrl builds the authorization URL from the state blob. When the provider requires PKCE but the state payload carries no pkce.codeChallenge/codeChallengeMethod (or the state string isn't a parseable state envelope, so parsedState is null), it refuses to construct a PKCE-less authorization request and throws InvalidOauthResponse. PKCE material is minted server-side in /oauth/preflight and embedded in state — its absence means the state didn't come from a current preflight.

Source

Thrown at packages/backend/server/src/plugins/oauth/providers/oidc.ts:252

      `OIDC discovery validation failed, retrying in ${delay}ms`
    );
  }

  private resetState() {
    this.#endpoints = null;
    this.#jwks = null;
  }

  getAuthUrl(state: string): string {
    const parsedState = this.parseStatePayload(state);
    const nonce = parsedState?.state ?? state;
    const pkce = parsedState?.pkce;

    if (
      this.requiresPkce &&
      (!pkce?.codeChallenge || !pkce.codeChallengeMethod)
    ) {
      throw new InvalidOauthResponse({
        reason: 'Missing PKCE challenge for OIDC authorization request',
      });
    }

    const query: JWTPayload = {
      client_id: this.config.clientId,
      redirect_uri: this.url.link('/oauth/callback'),
      scope: this.resolveScope(this.config.args?.scope),
      response_type: 'code',
      ...omit(
        this.config.args,
        'claim_id',
        'claim_email',
        'claim_name',
        'claim_email_verified'
      ),
      state,
      nonce,

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Always initiate login via POST /oauth/preflight and use the state/authorize URL it returns — never construct state yourself
  2. Invalidate cached/pending authorize URLs after upgrading the server; users mid-flow should restart login
  3. If the IdP mandates PKCE (e.g. config requires it), ensure the server version that created the state already supports PKCE
Defensive patterns

Strategy: validation

Validate before calling

// Only ever feed getAuthUrl the state string produced by POST /oauth/preflight
const { state, authorize_url } = await post('/oauth/preflight', {
  provider: 'OIDC', client: 'web', client_nonce: crypto.randomUUID(),
});
// verify it is the envelope format this server version issues
const parsed = JSON.parse(state);
if (!parsed.state || !parsed.pkce?.codeChallenge) {
  throw new Error('Preflight returned a state without PKCE — server/provider PKCE config mismatch');
}
window.location.href = authorize_url ?? provider.getAuthUrl(state);

Type guard

function isPkceStateEnvelope(s: string): boolean {
  try {
    const p = JSON.parse(s);
    return typeof p.state === 'string' && typeof p.pkce?.codeChallenge === 'string';
  } catch { return false; }
}

Try / catch

try {
  window.location.href = provider.getAuthUrl(state);
} catch (e) {
  if ((e as any).code === 'invalid_oauth_response' && /PKCE/i.test((e as any).args?.reason ?? '')) {
    return restartFromPreflight(); // state predates PKCE — mint a fresh one
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getAuthUrl with a raw/legacy state string: a state from a login started before the server upgrade that added PKCE, a state string hand-crafted by a third-party client, or a stale cached authorize URL being re-used after the IdP turned on PKCE enforcement.

Common situations: Server upgraded mid-flight — users with an open login tab carry old-format state; custom clients building their own authorize URLs instead of going through preflight; browser back-button resubmitting an old authorize URL.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/c30703dacb99dca6. Report an issue: GitHub.