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 →
HIGH AI PREVALENCE: MEDIUM auth.oauth.ropc-grant

OAuth token request uses the Resource Owner Password Credentials grant (grant_type=password).

Why AI tools produce this: AI coding tools produce this regularly, typically when prompted for a shortcut or a quick fix.

Why this matters

The app collects the user's password and replays it to the authorization server, exactly what OAuth was designed to avoid. It cannot support federation, MFA, or step-up auth, and any compromise of your service exposes raw user passwords.

The OAuth 2.0 Security BCP (RFC 9700 §2.4) forbids ROPC and OAuth 2.1 removes it entirely. Use the authorization-code flow with PKCE (grant_type=authorization_code) for user login, or client_credentials for machine-to-machine.

VULNERABLE
vulnerable.ts
// Resource Owner Password Credentials (ROPC) grant — deprecated by RFC 9700,
// removed in OAuth 2.1. The app handles the raw user password.

export async function loginObjectBody(username: string, password: string) {
  // ruleid: auth.oauth.ropc-grant
  const body = {
    grant_type: 'password',
    username,
    password,
    client_id: 'web-app',
  };
  return fetch('https://idp.example.com/oauth/token', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
}

export async function loginFormString(username: string, password: string) {
  // ruleid: auth.oauth.ropc-grant
  const body = `grant_type=password&username=${username}&password=${password}&client_id=web-app`;
  return fetch('https://idp.example.com/oauth/token', { method: 'POST', body });
}

export async function loginUrlSearchParams(username: string, password: string) {
  const params = new URLSearchParams();
  // ruleid: auth.oauth.ropc-grant
  params.append('grant_type', 'password');
  params.append('username', username);
  params.append('password', password);
  return fetch('https://idp.example.com/oauth/token', { method: 'POST', body: params });
}
SAFE
safe.ts
// Authorization-code and client-credentials grants — the supported flows.
// None of these collect or replay the user's password.

export async function exchangeCode(code: string, verifier: string) {
  // ok: auth.oauth.ropc-grant
  const body = {
    grant_type: 'authorization_code',
    code,
    code_verifier: verifier,
    client_id: 'web-app',
  };
  return fetch('https://idp.example.com/oauth/token', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
}

export async function machineToken() {
  // ok: auth.oauth.ropc-grant
  const body = 'grant_type=client_credentials&client_id=svc&client_secret=shh';
  return fetch('https://idp.example.com/oauth/token', { method: 'POST', body });
}

export async function refresh(refreshToken: string) {
  const params = new URLSearchParams();
  // ok: auth.oauth.ropc-grant
  params.append('grant_type', 'refresh_token');
  params.append('refresh_token', refreshToken);
  return fetch('https://idp.example.com/oauth/token', { method: 'POST', body: params });
}

// A field literally named `password` that is NOT an OAuth grant_type must not
// trip the rule.
export function resetForm(password: string) {
  return { action: 'reset', password };
}

// An OAuth library's own grant-type resolver binds the string to a local
// variable; it is the implementation of the grant, not an application sending a
// password token request. A bare assignment must not trip the rule.
export function guessGrantType(kwargs: Record<string, unknown>): string {
  // ok: auth.oauth.ropc-grant
  let grant_type = 'client_credentials';
  if ('code' in kwargs) {
    grant_type = 'authorization_code';
  } else if ('username' in kwargs && 'password' in kwargs) {
    grant_type = 'password';
  }
  return grant_type;
}

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.ropc-grant -- <reason>

References

https://datatracker.ietf.org/doc/html/rfc9700#section-2.4 ↗https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-2.4 ↗https://cwe.mitre.org/data/definitions/522.html ↗