mrkeyoor.com_
Thu 06 Aug 00:59 UTC
PyPISecurityupdated 05 Aug 2026

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.

Verdict

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.

API stability5/5The 3.x line has held its public API since 2019; new specs like the device flow arrived as additive modules under oauthlib.oauth2.rfc8628 rather than as reshuffles of existing classes.
Docs2/5The README admits the documentation is sparse and asks for pull requests. There is a decent OAuth 2 provider tutorial and a feature matrix, but the RequestValidator method contracts and error semantics are best learned from the source.
Maintenance3/5Pushed July 2026 with 89 open issues (117 counting PRs) and 3.3.1 as the current release, but the cadence is slow and community-driven; the project describes itself as busy and slow to reply, and fixes can sit for months.
Ecosystem5/5Around 78 million weekly downloads because requests-oauthlib, django-allauth, django-oauth-toolkit, Flask-Dance, and many SDKs depend on it, so it is one of the most widely installed security packages in Python.

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
Skip it if

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_url

state 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 403

validate_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

PackageRegistryPick it when
requests-oauthlibPyPIYou are a client calling an OAuth-protected API with requests and want a session that signs and refreshes for you; it wraps this library.
authlibPyPIYou 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-toolkitPyPIYou are turning a Django app into an OAuth 2 provider and want models, admin, and DRF permission classes instead of a validator interface.
pyjwtPyPIYou only need to sign and verify JWT access tokens and are not implementing an OAuth flow at all.