OAuth 2.0 is the authorization framework behind nearly every "Sign in with Google" button, third-party API integration, and machine-to-machine workflow on the modern web. If you write Python code that talks to external APIs, understanding how to implement OAuth 2.0 with the Requests library is an essential skill. This guide walks through the core grant types, shows you working code for each one, and covers the security practices that matter in production.
Before OAuth existed, integrating with a third-party service often meant handing over your username and password directly to the application that needed access. OAuth 2.0 replaced that pattern with a token-based system where the user grants limited, scoped permissions to a client application without ever exposing their credentials. The framework defines specific roles -- resource owner, client, authorization server, and resource server -- along with several grant types designed for different application architectures. In Python, the Requests library and its companion packages make it straightforward to implement each of these flows.
What OAuth 2.0 Is and Why It Matters
OAuth 2.0 is an authorization framework, not an authentication protocol. The distinction matters. Authentication verifies who you are. Authorization determines what you can access. OAuth 2.0 handles the second part. When you click "Sign in with GitHub" on a third-party app, OAuth 2.0 is the mechanism that lets that app request specific permissions (like reading your repositories) without ever seeing your GitHub password.
The protocol involves four roles that work together during every flow. The resource owner is typically the end user who owns the data. The client is your Python application requesting access to that data. The authorization server is the service (like Google or GitHub) that authenticates the user and issues tokens. And the resource server hosts the protected data your client wants to reach -- often the same system as the authorization server.
OAuth 2.0 uses access tokens -- short-lived credentials that represent the granted permissions -- instead of raw passwords. These tokens carry specific scopes that limit what the client can do, and they expire after a defined period, reducing the damage if one is ever compromised.
The OAuth 2.1 draft specification, which consolidates years of security best practices, is already being adopted by leading organizations. Key changes include making PKCE mandatory for all authorization code flows and formally removing insecure grant types like Implicit and Resource Owner Password Credentials (ROPC).
Setting Up Your Python Environment
You will need Python 3.8 or later and a few packages. The requests library handles HTTP communication, while requests-oauthlib provides a high-level interface for OAuth 2.0 flows built on top of the OAuthLib framework. For projects where you also need JWT handling or OpenID Connect support, authlib is a strong alternative that bundles everything into a single package.
# Install the core libraries
pip install requests requests-oauthlib
# Alternative: Authlib provides an all-in-one solution
# for OAuth 2.0, OpenID Connect, and JWT
pip install authlib
Before writing any code, you need to register your application with the OAuth 2.0 provider (Google, GitHub, or whichever service you plan to integrate with). This registration process gives you two critical values: a client ID that identifies your application and a client secret that acts as your application's password when communicating with the authorization server. You will also configure a redirect URI -- the URL where the provider sends the user after they approve or deny access.
Never hard-code your client secret directly in your source code. Use environment variables or a secrets manager. If your secret is exposed, an attacker can impersonate your application to the authorization server.
Implementing the Client Credentials Grant
The client credentials grant is the simplest OAuth 2.0 flow. It is designed for machine-to-machine communication where no end user is involved. Your application authenticates directly with the authorization server using its own client ID and secret, and receives an access token in return. This flow is commonly used for backend services, scheduled jobs, and internal API calls between microservices.
import requests
import os
# Load credentials from environment variables
CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
CLIENT_SECRET = os.environ["OAUTH_CLIENT_SECRET"]
TOKEN_URL = "https://auth.example.com/oauth/token"
def get_client_credentials_token():
"""Obtain an access token using the client credentials grant."""
response = requests.post(
TOKEN_URL,
data={
"grant_type": "client_credentials",
"scope": "read:data write:data",
},
auth=(CLIENT_ID, CLIENT_SECRET),
)
response.raise_for_status()
token_data = response.json()
return token_data["access_token"]
# Use the token to call a protected API
access_token = get_client_credentials_token()
headers = {"Authorization": f"Bearer {access_token}"}
api_response = requests.get(
"https://api.example.com/v1/resources",
headers=headers,
)
print(api_response.json())
The auth=(CLIENT_ID, CLIENT_SECRET) parameter uses HTTP Basic authentication to send credentials in the Authorization header, which is the method defined as client_secret_basic in the OAuth specification. Some providers expect credentials in the POST body instead (client_secret_post). Check your provider's documentation to confirm which method they require.
Implementing the Authorization Code Flow
The authorization code flow is the standard choice when your application acts on behalf of a user. It involves two steps: first, the user is redirected to the authorization server to grant permission; then your application exchanges the resulting authorization code for an access token. This two-step process keeps the access token out of the browser's address bar, which is a significant security advantage over older approaches.
The requests-oauthlib library simplifies this flow considerably. Here is how the full process looks in a Python script. In a real web application, you would handle the redirect through your web framework (Flask, Django, FastAPI), but the OAuth logic remains the same.
from requests_oauthlib import OAuth2Session
import os
CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
CLIENT_SECRET = os.environ["OAUTH_CLIENT_SECRET"]
AUTHORIZATION_URL = "https://auth.example.com/oauth/authorize"
TOKEN_URL = "https://auth.example.com/oauth/token"
REDIRECT_URI = "https://yourapp.com/callback"
SCOPE = ["read:user", "read:repos"]
# Step 1: Build the authorization URL and redirect the user
oauth = OAuth2Session(
CLIENT_ID,
redirect_uri=REDIRECT_URI,
scope=SCOPE,
)
authorization_url, state = oauth.authorization_url(AUTHORIZATION_URL)
print(f"Visit this URL to authorize: {authorization_url}")
# Save 'state' to verify later (prevents CSRF attacks)
# In a web app, store this in the user's session
# Step 2: After the user approves, they are redirected back
# with an authorization code in the query string
redirect_response = input("Paste the full redirect URL here: ")
# Step 3: Exchange the authorization code for an access token
token = oauth.fetch_token(
TOKEN_URL,
authorization_response=redirect_response,
client_secret=CLIENT_SECRET,
)
print(f"Access token: {token['access_token']}")
# Step 4: Use the session to make authenticated requests
response = oauth.get("https://api.example.com/v1/user")
print(response.json())
The state parameter is a randomly generated string that your application sends with the authorization request and verifies when the user is redirected back. This protects against cross-site request forgery (CSRF) attacks, where an attacker tries to trick a user into authorizing an unintended action. The requests-oauthlib library generates and validates this value automatically.
Adding PKCE to the Authorization Code Flow
Proof Key for Code Exchange (PKCE, pronounced "pixie") is a security extension that protects the authorization code from being intercepted and misused. Originally designed for mobile and native applications that cannot securely store a client secret, PKCE is now considered a requirement for all clients. The upcoming OAuth 2.1 specification mandates PKCE for every authorization code flow, regardless of whether the client is public or confidential.
PKCE works by creating a one-time secret for each authorization request. Your application generates a random string called the code_verifier, hashes it to produce a code_challenge, and sends the challenge with the initial authorization request. When your application later exchanges the authorization code for a token, it sends the original code_verifier. The authorization server hashes it and compares the result against the stored challenge. If they match, the code is legitimate. If an attacker intercepted the authorization code, they would not have the code_verifier and could not complete the exchange.
import hashlib
import base64
import os
from requests_oauthlib import OAuth2Session
CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
AUTHORIZATION_URL = "https://auth.example.com/oauth/authorize"
TOKEN_URL = "https://auth.example.com/oauth/token"
REDIRECT_URI = "https://yourapp.com/callback"
def generate_pkce_pair():
"""Generate a PKCE code verifier and challenge."""
# Create a cryptographically random 32-byte string
code_verifier = base64.urlsafe_b64encode(
os.urandom(32)
).rstrip(b"=").decode("utf-8")
# Hash the verifier using SHA-256
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode("utf-8")).digest()
).rstrip(b"=").decode("utf-8")
return code_verifier, code_challenge
# Generate the PKCE pair
code_verifier, code_challenge = generate_pkce_pair()
# Build the authorization URL with PKCE parameters
oauth = OAuth2Session(CLIENT_ID, redirect_uri=REDIRECT_URI)
authorization_url, state = oauth.authorization_url(
AUTHORIZATION_URL,
code_challenge=code_challenge,
code_challenge_method="S256",
)
print(f"Visit: {authorization_url}")
# After the user is redirected back...
redirect_response = input("Paste the redirect URL: ")
# Exchange code for token, including the code_verifier
token = oauth.fetch_token(
TOKEN_URL,
authorization_response=redirect_response,
code_verifier=code_verifier,
# No client_secret needed for public clients
)
If you are using Authlib instead of requests-oauthlib, PKCE support is built in. Create your session with code_challenge_method="S256" and Authlib generates and manages the verifier and challenge automatically.
Managing Tokens: Refresh, Expiry, and Secure Storage
Access tokens are deliberately short-lived. Depending on the provider, they may expire in as little as 15 minutes or up to an hour. When an access token expires, your application needs to obtain a new one without forcing the user through the authorization flow again. This is where refresh tokens come in.
A refresh token is a long-lived credential that your application can exchange for a new access token at any time. The authorization server issues a refresh token alongside the initial access token when you request the offline_access scope (or its provider-specific equivalent). Here is how to use it:
import requests
import time
import os
class OAuth2TokenManager:
"""Manages OAuth 2.0 tokens with automatic refresh."""
def __init__(self, token_url, client_id, client_secret):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self.access_token = None
self.refresh_token = None
self.expires_at = 0
def set_token(self, token_data):
"""Store token data from an OAuth 2.0 response."""
self.access_token = token_data["access_token"]
self.refresh_token = token_data.get("refresh_token")
# Calculate the absolute expiry time
expires_in = token_data.get("expires_in", 3600)
self.expires_at = time.time() + expires_in
def get_valid_token(self):
"""Return a valid access token, refreshing if needed."""
# Add a 60-second buffer to avoid edge-case expiry
if time.time() >= (self.expires_at - 60):
self._refresh()
return self.access_token
def _refresh(self):
"""Use the refresh token to obtain new credentials."""
if not self.refresh_token:
raise RuntimeError(
"No refresh token available. "
"Re-authorize the user."
)
response = requests.post(
self.token_url,
data={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
},
auth=(self.client_id, self.client_secret),
)
response.raise_for_status()
token_data = response.json()
self.set_token(token_data)
def authorized_request(self, method, url, **kwargs):
"""Make an HTTP request with a valid Bearer token."""
token = self.get_valid_token()
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {token}"
return requests.request(
method, url, headers=headers, **kwargs
)
Token storage is a critical security concern. For server-side applications, store tokens in an encrypted database or a dedicated secrets manager -- never in plain-text configuration files, client-side storage, or log files. If you are building a web application, the refresh token should live server-side only and should never be sent to the browser.
Modern best practices also recommend refresh token rotation, where each use of a refresh token returns a new refresh token and invalidates the old one. This limits the window of opportunity if a refresh token is ever compromised. Check whether your provider supports rotation and enable it when available.
Choosing the Right Grant Type
OAuth 2.0 defines several grant types, each designed for a specific application architecture. Selecting the wrong one can introduce security vulnerabilities or unnecessary complexity. The table below summarizes the flows you should consider and which ones to avoid.
| Grant Type | Use Case | Status |
|---|---|---|
| Authorization Code + PKCE | Web apps, mobile apps, SPAs, and native desktop applications acting on behalf of a user | Recommended for all user-facing flows |
| Client Credentials | Server-to-server communication, background jobs, microservices calling internal APIs | Recommended for machine-to-machine |
| Device Authorization | Input-limited devices like smart TVs, CLI tools, and IoT hardware | Recommended for its specific niche |
| Implicit | Previously used for SPAs; exposes tokens in browser URL fragments | Deprecated -- use Auth Code + PKCE |
| Resource Owner Password (ROPC) | Previously used for trusted first-party apps; requires users to hand credentials to the client | Deprecated -- use Auth Code + PKCE |
The core guidance is straightforward: if a user is involved, use the authorization code flow with PKCE. If only machines are talking to each other, use client credentials. The Implicit and ROPC grants are deprecated because they expose tokens and passwords in ways that modern security practices consider unacceptable. The OAuth 2.1 draft formally removes both.
Key Takeaways
- Use Authorization Code + PKCE for user-facing flows: This is the recommended grant type for web, mobile, and desktop applications. PKCE protects against authorization code interception and is required under the OAuth 2.1 draft specification for all clients, including confidential server-side apps.
- Use Client Credentials for machine-to-machine communication: When no end user is involved -- such as backend services calling internal APIs or scheduled jobs accessing resources -- the client credentials grant provides a simple, direct path to an access token.
- Implement proper token lifecycle management: Access tokens should be short-lived. Store refresh tokens securely on the server side, check for expiration before each request with a buffer window, and use refresh token rotation when your provider supports it.
- Never hard-code secrets or store tokens insecurely: Client secrets belong in environment variables or a secrets manager. Tokens should never appear in logs, URLs, or client-side storage. Use HTTPS for every request without exception.
- Avoid deprecated flows: The Implicit grant and Resource Owner Password Credentials grant are formally deprecated. Both expose credentials or tokens in ways that create unnecessary risk. Migrate any existing implementations to Authorization Code + PKCE.
OAuth 2.0 gives Python developers a standardized, secure way to connect applications to external services and APIs. The Requests library and its OAuth extensions handle the heavy lifting of building authorization URLs, exchanging codes for tokens, and managing credentials. By choosing the right grant type for your use case, adding PKCE to every authorization code flow, and treating tokens with the same care you would give to passwords, you build integrations that are both functional and resilient against common attack patterns.