Skip to content

OAuth2 Resource Owner Password Grant — Legacy First-Party Authentication

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about OAuth2 Resource Owner Password Grant. We cover key concepts, practical examples, and best practices to help you master this topic.

OAuth2 Resource Owner Password Credentials (ROPC) grant allows clients to exchange a user's username and password directly for tokens, making it suitable only for highly-trusted first-party applications.

What You'll Learn

ROPC implementation, when it is appropriate to use, security risks, Migration strategies to Authorization Code grant, and how to mitigate credential exposure.

Why It Matters

Many legacy applications and internal tools still use ROPC. Understanding it helps you maintain existing systems and plan migration paths to more secure flows.

Real-World Use

ROPC is appropriate for first-party mobile apps where the vendor controls both the app and the API. Durga Antivirus Pro uses ROPC in its internal admin panel where the login form submits directly to the auth server.

sequenceDiagram
    participant User as User
    participant Client as Client App
    participant Auth as Auth Server
    participant API as API Server

    User->>Client: Enter username + password
    Client->>Auth: POST /token
grant_type=password
username=...
password=... Auth->>Auth: Validate credentials Auth->>Client: { access_token, refresh_token, expires_in } Client->>API: API calls with Bearer token API->>Client: Protected resources

Code Example: ROPC Token Endpoint

import jwt, datetime, secrets, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = "ropc-secret"

# User store (use database in production)
users = {
    "analyst@durga.com": {
        "password_hash": hashlib.sha256("secure-pass-123".encode()).hexdigest(),
        "roles": ["analyst"],
        "mfa_enabled": True
    }
}

@app.route("/oauth/token", methods=["POST"])
def token():
    grant_type = request.form.get("grant_type")
    username = request.form.get("username", "")
    password = request.form.get("password", "")
    scope = request.form.get("scope", "openid profile")
    client_id = request.form.get("client_id")

    if grant_type != "password":
        return jsonify({"error": "unsupported_grant_type"}), 400

    user = users.get(username)
    if not user:
        return jsonify({"error": "invalid_grant"}), 401

    password_hash = hashlib.sha256(password.encode()).hexdigest()
    if password_hash != user["password_hash"]:
        return jsonify({"error": "invalid_grant"}), 401

    # Check MFA requirement
    if user.get("mfa_enabled"):
        mfa_code = request.form.get("mfa_code", "")
        if not verify_mfa(username, mfa_code):
            return jsonify({
                "error": "mfa_required",
                "error_description": "MFA code required for this account"
            }), 403

    access_token = jwt.encode({
        "sub": username,
        "roles": user["roles"],
        "scope": scope,
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1),
        "jti": secrets.token_hex(16)
    }, SECRET, algorithm="HS256")

    refresh_token = secrets.token_urlsafe(32)

    return jsonify({
        "access_token": access_token,
        "token_type": "Bearer",
        "expires_in": 3600,
        "refresh_token": refresh_token,
        "scope": scope
    })

Code Example: ROPC Client Implementation

import requests

class ROPCClient:
    def __init__(self, token_url, client_id, client_secret=None):
        self.token_url = token_url
        self.client_id = client_id
        self.client_secret = client_secret

    def login(self, username, password, mfa_code=None):
        data = {
            "grant_type": "password",
            "username": username,
            "password": password,
            "client_id": self.client_id
        }
        if self.client_secret:
            data["client_secret"] = self.client_secret
        if mfa_code:
            data["mfa_code"] = mfa_code

        resp = requests.post(self.token_url, data=data)

        if resp.status_code == 403 and "mfa_required" in resp.text:
            # Prompt user for MFA code
            mfa = input("MFA code required: ")
            return self.login(username, password, mfa)

        return resp.json()

# Usage
client = ROPCClient(
    token_url="https://auth.dodatech.com/oauth/token",
    client_id="durga-admin-panel"
)
tokens = client.login("analyst@durga.com", "secure-pass-123")

Code Example: Migration Path — ROPC to Authorization Code

class HybridAuthClient:
    """Client that supports both ROPC and Auth Code flows."""

    def __init__(self, config):
        self.config = config
        self.auth_method = self._detect_auth_method()

    def _detect_auth_method(self):
        """Check if auth server supports PKCE."""
        resp = requests.get(f"{self.config['issuer']}/.well-known/openid-configuration")
        capabilities = resp.json()
        if "authorization_endpoint" in capabilities:
            # Server supports authorization code flow
            return "authorization_code"
        return "password"

    def authenticate(self):
        if self.auth_method == "authorization_code":
            return self._auth_code_flow()
        return self._ropc_flow()

    def _ropc_flow(self):
        """Legacy ROPC flow."""
        resp = requests.post(f"{self.config['issuer']}/token", data={
            "grant_type": "password",
            "username": self.config["username"],
            "password": self.config["password"],
            "client_id": self.config["client_id"]
        })
        return resp.json()

    def _auth_code_flow(self):
        """Modern PKCE flow."""
        # Implement PKCE flow
        verifier = secrets.token_urlsafe(64)
        challenge = base64.urlsafe_b64encode(
            hashlib.sha256(verifier.encode()).digest()
        ).rstrip(b'=').decode()

        auth_url = (
            f"{self.config['issuer']}/authorize?"
            f"response_type=code&client_id={self.config['client_id']}"
            f"&code_challenge={challenge}&code_challenge_method=S256"
        )
        print(f"Open in browser: {auth_url}")
        auth_code = input("Enter authorization code: ")

        resp = requests.post(f"{self.config['issuer']}/token", data={
            "grant_type": "authorization_code",
            "code": auth_code,
            "client_id": self.config["client_id"],
            "code_verifier": verifier,
            "redirect_uri": self.config["redirect_uri"]
        })
        return resp.json()

Common Mistakes

1. Using ROPC for Third-Party Applications

Third-party apps should never see user passwords. ROPC is for first-party applications only. Third parties must use Authorization Code grant.

2. Storing User Credentials Client-Side

ROPC exchanges credentials for tokens immediately. Never cache the username and password on the client side. Use refresh tokens for ongoing access.

3. No MFA Support

ROPC without MFA support is a security risk. If the user has MFA enabled, the token endpoint should require an MFA code as an additional parameter.

4. Ignoring the Refresh Token

ROPC issues both access and refresh tokens. Use refresh tokens for ongoing access instead of asking for the password again.

5. Not Rate-Limiting the Token Endpoint

ROPC endpoints receive plaintext passwords. Rate limit aggressively to prevent brute force attacks. Consider account lockout after failed attempts.

Practice Questions

  1. When is ROPC appropriate for use?
  2. Why is ROPC considered less secure than Authorization Code?
  3. How does ROPC handle MFA?
  4. What is the migration path from ROPC to Authorization Code?
  5. Why should ROPC responses include refresh tokens?

Answers:

  1. Only for highly-trusted first-party applications where the vendor controls both client and server, such as official mobile apps and internal admin panels.
  2. The client sees the user's password. In Authorization Code, the user authenticates directly with the authorization server, and the client never sees the password.
  3. The token endpoint can accept an additional mfa_code parameter. If MFA is required but not provided, return a 403 with mfa_required error.
  4. Migrate to Authorization Code with PKCE. The auth server adds an /authorize endpoint, and the client switches from collecting credentials to redirecting users.
  5. So the client can obtain new access tokens without repeatedly asking for the password. Refresh tokens reduce credential exposure.

Challenge: Build a legacy-to-modern migration script that supports both ROPC and Authorization Code flows, automatically detecting server capabilities and falling back gracefully.

FAQ

Is ROPC deprecated?

The OAuth2 Security BCP discourages ROPC, but it is not formally deprecated. Many identity providers still support it for migration purposes.

Does ROPC work with MFA?

Yes, but MFA must be handled as an additional parameter or step. The user provides the MFA code alongside the password.

Can I use ROPC with social login?

No. Social login requires browser redirects. Use Authorization Code grant with social identity providers.

How do I log all ROPC authentication attempts?

Log the username, client_id, IP address, timestamp, and success/failure status in the token endpoint. Never log the password.

What is the alternative for command-line tools?

Device Authorization Grant (device flow) is the recommended alternative. The user authenticates on a separate device with a code.

Mini Project

Build a ROPC token endpoint with MFA support, Rate Limiting, and a client that automatically detects whether to use ROPC or Authorization Code based on server capabilities.

What's Next

Now learn about OAuth2 Implicit Flow and Why It Is Deprecated to understand the history and migration paths.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro