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.py.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. In Python this covers a requests/httpx/urllib body, an OAuth client call, or a URL-encoded body string.

VULNERABLE
vulnerable.py
"""OAuth token requests using the forbidden ROPC (password) grant."""

import requests
import httpx

TOKEN_URL = "https://idp.example.com/oauth/token"


def login_requests(username: str, password: str):
    # ruleid: auth.py.oauth.ropc-grant
    return requests.post(
        TOKEN_URL,
        data={"grant_type": "password", "username": username, "password": password},
    )


def login_httpx(username: str, password: str):
    # ruleid: auth.py.oauth.ropc-grant
    return httpx.post(TOKEN_URL, data={'grant_type': 'password', 'username': username})


def login_body_string(username: str, password: str):
    # ruleid: auth.py.oauth.ropc-grant
    body = "grant_type=password&username=alice&password=secret"
    return requests.post(TOKEN_URL, data=body)


def login_pairs(username: str, password: str):
    # ruleid: auth.py.oauth.ropc-grant
    return requests.post(TOKEN_URL, data=[("grant_type", "password"), ("username", username)])
SAFE
safe.py
"""OAuth token requests using safe grant types — no ROPC."""

import requests

TOKEN_URL = "https://idp.example.com/oauth/token"


# ok: auth.py.oauth.ropc-grant -- authorization-code flow, the recommended login
def exchange_code(code: str, verifier: str):
    return requests.post(
        TOKEN_URL,
        data={"grant_type": "authorization_code", "code": code, "code_verifier": verifier},
    )


# ok: auth.py.oauth.ropc-grant -- machine-to-machine client credentials
def service_token():
    return requests.post(TOKEN_URL, data={"grant_type": "client_credentials"})


# ok: auth.py.oauth.ropc-grant -- a password-reset endpoint is not the ROPC grant
def reset_password(email: str):
    return requests.post("https://idp.example.com/account", data={"grant_type": "password_reset", "email": email})


# ok: auth.py.oauth.ropc-grant -- grant type comes from a variable, not a literal
def dynamic_grant(grant: str):
    return requests.post(TOKEN_URL, data={"grant_type": grant})


# ok: auth.py.oauth.ropc-grant -- a 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 (cf. Authlib _guess_grant_type).
def _guess_grant_type(kwargs):
    if "code" in kwargs:
        grant_type = "authorization_code"
    elif "username" in kwargs and "password" in kwargs:
        grant_type = "password"
    else:
        grant_type = "client_credentials"
    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.py.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 ↗