oauthlib
OAuthLib implements the OAuth 1.0 and OAuth 2.0 specs as pure logic, with no opinion about how you make HTTP requests or which web framework you serve. You hand it strings (a URI, a method, a body, headers) and it hands back signed strings: an authorization URL to redirect to, a token request body to POST, a parsed token dict, or on the provider side a full response tuple of headers, body, and status code. It covers OAuth 1 request signing (HMAC, RSA, plaintext), the OAuth 2 grant types including authorization code with PKCE, client credentials, refresh, and the device flow from RFC 8628, plus OpenID Connect. Almost nobody installs it deliberately: it arrives as the engine under requests-oauthlib, django-oauth-toolkit, django-allauth, and Flask-Dance.
The spec-correct OAuth engine most of Python is already running transitively, and the right pick when you are building a provider or a client for an unusual transport. If you are just calling an API or bolting OAuth onto Django or Flask, use requests-oauthlib, Authlib, or django-oauth-toolkit instead and let them talk to this.
Use it if
- You are building an OAuth provider and want spec-correct parameter validation, error responses, and grant-type plumbing without writing RFC 6749 section by section yourself
- You need OAuth 1.0a request signing, which the modern ecosystem has largely abandoned but Twitter-era, Atlassian, and many enterprise and telecom APIs still require
- You are writing a client for an HTTP library that is not requests (httpx, aiohttp, a socket-level client) and need the signing and token-parsing logic separated from transport
- You want the device authorization flow or a non-standard grant on the provider side and need to subclass the grant type rather than fight a framework's fixed set of endpoints
- You are consuming somebody else's API from a normal app: requests-oauthlib or Authlib gives you a session object that handles the whole dance, while raw oauthlib makes you assemble every request by hand
- You want an OAuth provider inside Django or Flask: django-oauth-toolkit and Authlib's framework integrations already wire storage, models, and views to this logic, and rebuilding that on bare oauthlib is weeks of work you will maintain forever
- You want async: the whole library is synchronous string manipulation, which is fine, but there are no async validator hooks, so your RequestValidator subclass must do database lookups synchronously or bridge them yourself
- You need a modern all-in-one identity stack: OAuthLib has no JWT implementation of its own (signed tokens need the pyjwt extra), no JWKS fetching, and its OpenID Connect support lags newer specs compared with Authlib
- You want fast answers when stuck: the maintainers say so themselves in the README, the docs are thin outside the tutorial, and provider-side questions often end with reading the source of the grant type you are using
Setup reality
pip install oauthlib is dependency-free, which is the trap: the features people actually need live behind extras. RSA signatures and signed JWT bearer tokens need pip install oauthlib[signedtoken], which pulls cryptography and pyjwt. Local development over http raises InsecureTransportError until you set OAUTHLIB_INSECURE_TRANSPORT=1, and a provider that changes the granted scope raises a warning until you set OAUTHLIB_RELAX_TOKEN_SCOPE=1. On the provider side the real cost is the RequestValidator subclass: it is a large abstract class where every unimplemented method silently returns False and your flow fails with a generic invalid_request, so you implement, test, and repeat until the errors stop. Version 3.x moved several imports around compared to the 2.x examples still floating around in blog posts.
Patterns
Build an authorization code flow clientauthorization-code-client
from oauthlib.oauth2 import WebApplicationClient
client = WebApplicationClient("my-client-id")
auth_url = client.prepare_request_uri(
"https://provider.example/authorize",
redirect_uri="https://app.example/callback",
scope=["read", "write"],
state="opaque-random-value",
)
# redirect the browser to auth_urlstate is not optional in practice: you must store it in the session and compare on the callback, otherwise you have a CSRF hole. oauthlib generates nothing for you here.
Exchange the callback code for a tokenexchange-code-for-token
import requests
client.parse_request_uri_response(
"https://app.example/callback?code=abc&state=opaque-random-value",
state="opaque-random-value",
)
body = client.prepare_request_body(
code=client.code,
redirect_uri="https://app.example/callback",
client_secret="my-secret",
)
res = requests.post("https://provider.example/token", data=dict(
p.split("=") for p in body.split("&")))
token = client.parse_request_body_response(res.text)parse_request_uri_response raises MismatchingStateError if state does not match, which is the check you want. prepare_request_body returns a urlencoded string, so most people post it with the form content type rather than splitting it apart.
Add PKCE to a public clientpkce-flow
from oauthlib.oauth2 import WebApplicationClient
client = WebApplicationClient("mobile-client-id")
verifier = client.create_code_verifier(96)
challenge = client.create_code_challenge(verifier, "S256")
auth_url = client.prepare_request_uri(
"https://provider.example/authorize",
redirect_uri="app://callback",
code_challenge=challenge,
code_challenge_method="S256",
)
# later, on token exchange:
body = client.prepare_request_body(code=code, code_verifier=verifier)Pass "S256" explicitly. create_code_challenge defaults to the plain method, which sends the verifier itself and gives you no protection at all.
Machine-to-machine client credentials grantclient-credentials
from oauthlib.oauth2 import BackendApplicationClient
import requests
client = BackendApplicationClient(client_id="svc-id")
body = client.prepare_request_body(scope=["jobs:write"])
res = requests.post(
"https://provider.example/token",
data=body,
headers={"Content-Type": "application/x-www-form-urlencoded"},
auth=("svc-id", "svc-secret"),
)
token = client.parse_request_body_response(res.text)parse_request_body_response stores the token on the client and raises the matching OAuth2Error subclass (InvalidClientError, InvalidScopeError) when the provider returns an error body instead of returning a dict you have to inspect.
Attach a bearer token to an outgoing requestsign-outgoing-request
from oauthlib.oauth2 import TokenExpiredError
try:
uri, headers, body = client.add_token(
"https://api.example/v1/things",
http_method="GET",
headers={"Accept": "application/json"},
)
except TokenExpiredError:
refresh_body = client.prepare_refresh_body(refresh_token=stored_refresh)add_token checks the stored expires_at itself and raises TokenExpiredError rather than sending a request you know will fail. Clock skew between you and the provider is your problem to handle.
Sign an OAuth 1.0a requestoauth1-signing
from oauthlib.oauth1 import Client, SIGNATURE_HMAC_SHA256
client = Client(
"consumer-key",
client_secret="consumer-secret",
resource_owner_key="token",
resource_owner_secret="token-secret",
signature_method=SIGNATURE_HMAC_SHA256,
)
uri, headers, body = client.sign(
"https://api.example/1.1/statuses.json", http_method="GET")The signature covers the exact URI and body you pass, so signing before your HTTP library appends query parameters or rewrites the URL produces a signature the server rejects. Sign last.
Run the device authorization flowdevice-flow
from oauthlib.oauth2 import DeviceClient
import requests, time
client = DeviceClient("tv-client-id")
uri = client.prepare_request_uri("https://provider.example/device/code", scope=["read"])
start = requests.post(uri).json()
print(f"Go to {start['verification_uri']} and enter {start['user_code']}")
body = client.prepare_request_body(device_code=start["device_code"])
while True:
res = requests.post("https://provider.example/token", data=body)
if res.ok:
token = client.parse_request_body_response(res.text)
break
time.sleep(start.get("interval", 5))Handle AuthorizationPendingError and SlowDownError from oauthlib.oauth2 instead of polling blindly; SlowDownError means the provider wants you to add five seconds to the interval.
Implement the provider-side RequestValidatorprovider-validator
from oauthlib.oauth2 import RequestValidator
class MyValidator(RequestValidator):
def validate_client_id(self, client_id, request, *args, **kwargs):
return Client.objects.filter(id=client_id).exists()
def validate_redirect_uri(self, client_id, redirect_uri, request, *a, **kw):
return redirect_uri in registered_uris(client_id)
def save_authorization_code(self, client_id, code, request, *a, **kw):
AuthCode.objects.create(client_id=client_id, code=code["code"],
scopes=" ".join(request.scopes))Every method you do not override returns False or raises NotImplementedError, and the flow then fails with a generic invalid_request. Enable logging on the oauthlib logger while building this, or you will be guessing which hook said no.
Serve authorization and token endpointsprovider-endpoints
from oauthlib.oauth2 import WebApplicationServer
server = WebApplicationServer(MyValidator())
headers, body, status = server.create_authorization_response(
uri, http_method="POST", body=post_body, headers=req_headers,
scopes=["read"], credentials={"user": current_user},
)
headers, body, status = server.create_token_response(
uri, http_method="POST", body=post_body, headers=req_headers,
credentials={},
)These return plain tuples, so you convert them into your framework's response object yourself. credentials is how you pass the logged-in user through to the validator; it ends up on request.user.
Verify a bearer token on a protected endpointprotect-resource
from oauthlib.oauth2 import ResourceEndpoint, BearerToken
endpoint = ResourceEndpoint(
default_token="Bearer",
token_types={"Bearer": BearerToken(MyValidator())},
)
valid, oauth_request = endpoint.verify_request(
uri, http_method="GET", body="", headers=req_headers, scopes=["read"])
if not valid:
return 403validate_bearer_token on your validator is expected to attach request.user, request.client, and request.scopes when it returns True; downstream code reads them off the returned oauth_request.
Allow plain http during local developmentinsecure-transport-dev
import os
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
# optional, when the provider returns a different scope than requested
os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1"Set it before importing oauthlib, and never in production: it disables the check that raises InsecureTransportError on http URLs. Guard it behind a DEBUG flag so it cannot ship.
Catch the specific OAuth error the provider returnedhandle-oauth-errors
from oauthlib.oauth2 import (
InvalidGrantError, InvalidClientError, MismatchingStateError, OAuth2Error)
try:
token = client.parse_request_body_response(res.text)
except InvalidGrantError:
return redirect(login_again) # code reused or expired
except InvalidClientError:
log.error("client credentials rejected")
raise
except OAuth2Error as exc:
log.warning("oauth failed: %s %s", exc.error, exc.description)Every error carries .error, .description, and .status_code taken from the provider response, which is far more useful in logs than the exception class name alone.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| requests-oauthlib | PyPI | You are a client calling an OAuth-protected API with requests and want a session that signs and refreshes for you; it wraps this library. |
| authlib | PyPI | You want one maintained package covering OAuth 1, OAuth 2, OpenID Connect, JWT, and JWK, with ready integrations for Django, Flask, Starlette, and httpx. |
| django-oauth-toolkit | PyPI | You are turning a Django app into an OAuth 2 provider and want models, admin, and DRF permission classes instead of a validator interface. |
| pyjwt | PyPI | You only need to sign and verify JWT access tokens and are not implementing an OAuth flow at all. |