v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
MEDIUM AI PREVALENCE: HIGH auth.oauth.no-state-validation

OAuth callback handler reads state from the request but never compares it to a stored value.

Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.

Why this matters

Sending state on the authorize call is half of the CSRF mitigation; verifying it on the callback is the other half.

Compare the received state to the value you stored before redirecting (session, signed cookie, or Redis). Reject the callback if it's missing or doesn't match.

VULNERABLE
vulnerable.ts
interface Req {
  query: { state?: string; code?: string };
  body: { state?: string };
}

export function badCallback(req: Req) {
  // ruleid: auth.oauth.no-state-validation
  const state = req.query.state;
  console.log(`received state ${state}`);
  return req.query.code;
}

export function badCallback2(req: Req) {
  // ruleid: auth.oauth.no-state-validation
  return req.body.state;
}
SAFE
safe.ts
interface Req {
  query: { state?: string };
  session: { oauth_state?: string };
}

declare function verifyState(s?: string): boolean;

// ok: auth.oauth.no-state-validation
export function goodCallback(req: Req) {
  if (req.session.oauth_state !== req.query.state) {
    throw new Error('CSRF: state mismatch');
  }
  return true;
}

// ok: auth.oauth.no-state-validation -- reversed operand order is still validation
export function goodCallbackReversed(req: Req) {
  if (req.query.state !== req.session.oauth_state) {
    throw new Error('CSRF: state mismatch');
  }
  return true;
}

// ok: auth.oauth.no-state-validation -- validated via a helper
export function goodCallbackHelper(req: Req) {
  if (!verifyState(req.query.state)) {
    throw new Error('CSRF: state mismatch');
  }
  return true;
}

// ok: auth.oauth.no-state-validation -- state captured into a local, then validated
export function goodCallbackViaLocal(req: Req) {
  const state = req.query.state;
  if (req.session.oauth_state !== state) {
    throw new Error('CSRF: state mismatch');
  }
  return true;
}

// ok: auth.oauth.no-state-validation -- searchParams read into a local, then validated
export function goodCallbackSearchParams(url: URL, stored?: string) {
  const state = url.searchParams.get('state');
  if (!stored || stored !== state) {
    throw new Error('CSRF: state mismatch');
  }
  return true;
}

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

// oauthlint-disable-next-line auth.oauth.no-state-validation -- <reason>

References

https://datatracker.ietf.org/doc/html/rfc6749#section-10.12 ↗