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.go.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. With golang.org/x/oauth2, avoid Config.PasswordCredentialsToken and use AuthCodeURL / Exchange instead.

VULNERABLE
vulnerable.go
package main

import (
	"context"
	"net/http"
	"net/url"
	"strings"

	"golang.org/x/oauth2"
)

// golang.org/x/oauth2 ROPC helper — the password grant.
func loginOAuth2(conf *oauth2.Config, username, password string) (*oauth2.Token, error) {
	// ruleid: auth.go.oauth.ropc-grant
	return conf.PasswordCredentialsToken(context.Background(), username, password)
}

// Hand-rolled token request via url.Values builder.
func loginForm(username, password string) (*http.Response, error) {
	v := url.Values{}
	v.Set("grant_type", "password")
	v.Set("username", username)
	v.Set("password", password)
	// ruleid: auth.go.oauth.ropc-grant
	return http.PostForm("https://issuer.example.com/oauth/token", v)
}

// url.Values composite literal carrying the password grant.
func loginLiteral(username, password string) (*http.Response, error) {
	// ruleid: auth.go.oauth.ropc-grant
	v := url.Values{"grant_type": {"password"}, "username": {username}, "password": {password}}
	return http.PostForm("https://issuer.example.com/oauth/token", v)
}

// URL-encoded body string built by hand.
func loginRawBody(username, password string) (*http.Request, error) {
	// ruleid: auth.go.oauth.ropc-grant
	body := "grant_type=password&username=" + username + "&password=" + password
	return http.NewRequest("POST", "https://issuer.example.com/oauth/token", strings.NewReader(body))
}

func main() {}
SAFE
safe.go
package main

import (
	"context"
	"net/http"
	"net/url"
	"strings"

	"golang.org/x/oauth2"
)

// Safe: authorization-code exchange — the recommended user-login flow.
func loginAuthCode(conf *oauth2.Config, code string) (*oauth2.Token, error) {
	// ok: auth.go.oauth.ropc-grant
	return conf.Exchange(context.Background(), code)
}

// Safe: client-credentials grant for machine-to-machine — not the password
// grant. A `grant_type` field carrying a different value must not match.
func loginClientCreds() (*http.Response, error) {
	v := url.Values{}
	// ok: auth.go.oauth.ropc-grant
	v.Set("grant_type", "client_credentials")
	v.Set("client_id", "svc")
	return http.PostForm("https://issuer.example.com/oauth/token", v)
}

// Safe trap: a password-reset endpoint whose grant_type prefix-matches
// "password" but is a distinct, bounded value.
func resetPassword() (*http.Response, error) {
	// ok: auth.go.oauth.ropc-grant
	body := "grant_type=password_reset&email=user@example.com"
	return http.Post("https://issuer.example.com/reset", "application/x-www-form-urlencoded", strings.NewReader(body))
}

func main() {}

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.go.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 ↗