requests-oauthlib
requests-oauthlib glues the oauthlib protocol library onto requests so you can talk to OAuth-protected APIs with the requests interface you already know. OAuth1Session and OAuth2Session are subclasses of requests.Session: they sign OAuth 1 requests, run the OAuth 2 authorization code dance (authorization_url, then fetch_token), attach bearer tokens to every call, and can refresh expired tokens automatically. It covers the main OAuth 2 grants (authorization code with PKCE, client credentials, legacy password) plus full OAuth 1. It exists because most Python SDKs for Twitter-era and Google-era APIs needed exactly this and nothing more.
Still the shortest path from requests to an OAuth-protected API, and its 84M weekly downloads are baked into countless SDKs, but the project moves slowly and OAuth keeps moving. Fine to keep in existing code; for new projects Authlib is the safer bet.
Use it if
- You call an OAuth 2 API from requests and want the authorization code flow (authorization_url, state check, fetch_token) handled in a few lines
- You need automatic token refresh on long-lived integrations, with a callback to persist the new token when it rotates
- You still have to integrate an OAuth 1.0a API (some financial, government, and legacy platforms), where this and oauthlib remain the standard Python pair
- You do machine-to-machine calls with the client credentials grant and just want a session object that stays authenticated
- Maintenance pace matters to you: version 2.0.0 shipped in March 2024, the last repo push was June 2025, and 128 issues and PRs sit open, so fixes and new spec features arrive slowly
- You are starting fresh and want modern OAuth (RFC-current PKCE handling, JWT client auth, OpenID Connect, async): Authlib covers all of it for both requests and httpx and is actively developed
- Your provider has an official SDK: google-auth for Google or msal for Microsoft handle their token quirks, caching, and rotation better than a generic session will
- Your app is async: this is requests underneath, fully blocking, and there is no asyncio story at all
Setup reality
pip install requests-oauthlib is pure Python and pulls just oauthlib and requests. The friction is all protocol bookkeeping: local testing over http fails with InsecureTransportError until you export OAUTHLIB_INSECURE_TRANSPORT=1, providers that return slightly nonstandard token responses need workarounds from the documented compliance-fixes module, and auto-refresh silently does nothing unless you pass auto_refresh_url, auto_refresh_kwargs, and a token_updater callback together. Mismatched scopes between request and response raise warnings you have to read the docs to interpret, and OAuth 1 signatures fail on clock skew with errors that never mention clocks.
Patterns
OAuth 2 authorization code flowauth-code-flow
from requests_oauthlib import OAuth2Session
oauth = OAuth2Session(
client_id,
redirect_uri="https://myapp.example/callback",
scope=["read", "write"],
)
authorization_url, state = oauth.authorization_url(
"https://provider.example/oauth/authorize"
)
print("Visit:", authorization_url)
# after the user is redirected back:
token = oauth.fetch_token(
"https://provider.example/oauth/token",
client_secret=client_secret,
authorization_response=callback_url,
)Store state between the two steps and pass it when rebuilding the session in a web app, or the CSRF check fails with MismatchingStateError.
Call an API with an existing tokencall-api-with-token
from requests_oauthlib import OAuth2Session
token = {
"access_token": "abc123",
"token_type": "Bearer",
}
oauth = OAuth2Session(client_id, token=token)
resp = oauth.get("https://api.example.com/v1/me")
resp.raise_for_status()The session is a requests.Session, so headers, timeouts, and adapters work as usual; the bearer header is added on every request.
Refresh tokens automaticallyauto-refresh-token
def save_token(token):
db.store(user_id, token)
oauth = OAuth2Session(
client_id,
token=stored_token,
auto_refresh_url="https://provider.example/oauth/token",
auto_refresh_kwargs={
"client_id": client_id,
"client_secret": client_secret,
},
token_updater=save_token,
)
resp = oauth.get("https://api.example.com/v1/data")All three arguments are required; with a refresh URL but no token_updater the session raises TokenUpdated instead of continuing.
Machine-to-machine client credentials grantclient-credentials
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session
client = BackendApplicationClient(client_id=client_id)
oauth = OAuth2Session(client=client)
token = oauth.fetch_token(
token_url="https://provider.example/oauth/token",
client_id=client_id,
client_secret=client_secret,
)There is no user or redirect in this grant; the client object choice is what switches OAuth2Session between grant types.
Authorization code with PKCEpkce-flow
from requests_oauthlib import OAuth2Session
oauth = OAuth2Session(
client_id,
redirect_uri="https://myapp.example/callback",
pkce="S256",
)
authorization_url, state = oauth.authorization_url(auth_base_url)
# ... user authorizes ...
token = oauth.fetch_token(
token_url,
authorization_response=callback_url,
include_client_id=True,
)The pkce parameter arrived in 2.0.0; the session generates and stores the code verifier itself, so upgrade before copying this.
Legacy password grantpassword-grant
from oauthlib.oauth2 import LegacyApplicationClient
from requests_oauthlib import OAuth2Session
oauth = OAuth2Session(client=LegacyApplicationClient(client_id=client_id))
token = oauth.fetch_token(
token_url="https://provider.example/oauth/token",
username="user@example.com",
password="secret",
client_id=client_id,
client_secret=client_secret,
)This grant is deprecated by the OAuth 2 security best practices; use it only against internal or legacy providers that offer nothing else.
Call an OAuth 1 APIoauth1-session
from requests_oauthlib import OAuth1Session
session = OAuth1Session(
client_key,
client_secret=client_secret,
resource_owner_key=access_token,
resource_owner_secret=access_token_secret,
)
resp = session.get("https://api.provider.example/1.1/account.json")Every request is HMAC-signed; if the provider rejects signatures, check your server clock first because skew breaks the timestamp check.
Full OAuth 1 token danceoauth1-obtain-tokens
from requests_oauthlib import OAuth1Session
session = OAuth1Session(client_key, client_secret=client_secret,
callback_uri="https://myapp.example/callback")
session.fetch_request_token("https://provider.example/oauth/request_token")
url = session.authorization_url("https://provider.example/oauth/authorize")
print("Visit:", url)
# after redirect back:
session.parse_authorization_response(callback_url)
tokens = session.fetch_access_token("https://provider.example/oauth/access_token")The session carries the request token internally between steps, so in a web app you must persist and rebuild it across the redirect.
Use OAuth 1 as a plain requests auth objectoauth1-as-auth
import requests
from requests_oauthlib import OAuth1
auth = OAuth1(client_key, client_secret,
resource_owner_key, resource_owner_secret)
resp = requests.get("https://api.provider.example/resource", auth=auth)OAuth1 works anywhere requests accepts auth=, which is handy when you already have a configured session or use requests.get directly.
Allow http:// during local developmentlocal-http-testing
import os
# oauthlib refuses plain http by default
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
# now http://localhost callbacks work in fetch_token()Set this only in development; in production keep https so InsecureTransportError keeps protecting real tokens.
Tolerate providers that change scopesrelax-scope-check
import os
# provider returns different scopes than requested
os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1"
token = oauth.fetch_token(token_url, client_secret=client_secret,
authorization_response=callback_url)Facebook and a few other providers rewrite scopes in the token response; without this flag oauthlib raises a scope-changed warning or error.
Apply a compliance fix for a quirky providerfix-nonstandard-provider
from requests_oauthlib import OAuth2Session
from requests_oauthlib.compliance_fixes import facebook_compliance_fix
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri)
oauth = facebook_compliance_fix(oauth)
token = oauth.fetch_token(token_url, client_secret=client_secret,
authorization_response=callback_url)The compliance_fixes module patches known nonstandard token responses; check it before writing your own hook for a misbehaving provider.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| authlib | PyPI | You want an actively maintained, spec-complete OAuth and OIDC client with requests and httpx integrations |
| google-auth | PyPI | You authenticate against Google APIs, where the official library handles service accounts and token caching |
| msal | PyPI | You authenticate against Microsoft Entra or Office 365; it manages that ecosystem's token flows directly |
| oauthlib | PyPI | You are building a provider or need raw protocol signing without the requests session layer |