OAuth2 Resource Owner Password Grant — Legacy First-Party Authentication
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
- When is ROPC appropriate for use?
- Why is ROPC considered less secure than Authorization Code?
- How does ROPC handle MFA?
- What is the migration path from ROPC to Authorization Code?
- Why should ROPC responses include refresh tokens?
Answers:
- Only for highly-trusted first-party applications where the vendor controls both client and server, such as official mobile apps and internal admin panels.
- 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.
- The token endpoint can accept an additional mfa_code parameter. If MFA is required but not provided, return a 403 with mfa_required error.
- Migrate to Authorization Code with PKCE. The auth server adds an /authorize endpoint, and the client switches from collecting credentials to redirecting users.
- 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
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