mrkeyoor.com_
Sun 20 Sept 11:43 UTC
PyPISecurityupdated 20 Sept 2026

oauthlib review

oauthlib 3.3.1 implements OAuth 1 signing, OAuth 2 client and provider protocol logic, parts of OpenID Connect provider behavior, and the RFC 8628 device flow. It deliberately owns no HTTP connection, consent page, client database, or token store. Client objects prepare URLs, headers, and form bodies for another transport. Server endpoints call your `RequestValidator` and return response components for a web framework. Our install was one pure-Python package using 1 MB. The 3.3.1 patch corrects `expires_in` parsing broken in 3.3.0 and removes example files that were being copied into `site-packages`.

Verdict

oauthlib 3.3.1 installed as one 1 MB package in 0.2 seconds in our sandbox, with 0 audit findings and no `py.typed` marker. Use it when you need transport-neutral OAuth protocol pieces and can own storage, HTTP, and security policy; most application clients should start with a higher-level integration.

We installed it

Lab card: what happened when we installed oauthlibScreenshot of oauthlib documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport oauthlib in 0.11s · pure Python · requires Python >=3.8
Known vulns0(pip-audit)

Answers from our run

Does oauthlib install cleanly?

Yes. In a fresh container with an empty cache, pip install oauthlib finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does oauthlib need to run?

Python >=3.8, and nothing compiled: it is pure Python. In our run import oauthlib succeeded in 0.11s.

oauthlib or requests-oauthlib: which should you use?

requests-oauthlib: Use it when requests should send, sign, and refresh OAuth calls through a session. oauthlib 3.3.1 installed as one 1 MB package in 0.2 seconds in our sandbox, with 0 audit findings and no py.typed marker.

When should you not use oauthlib?

The requirement is ordinary sign-in through a known provider. Authlib or a framework adapter also handles discovery, sessions, HTTP calls, and token persistence.

API stability4/5The 3.x line retains its client classes, OAuth 1 signer, provider endpoint tuples, error types, and broad `RequestValidator` contract. Version 3.3 added device authorization components and PKCE corrections without replacing the main flows. The quick 3.3.1 fix for a 3.3.0 `expires_in` regression is also a reminder that stable signatures do not guarantee unchanged token timing behavior.
Docs3/5Read the Docs has a feature matrix, client and provider sections, grant examples, OAuth 1 material, and API pages. The README clearly says the library is transport-neutral and directs `requests` users to `requests-oauthlib`. Maintainers also call the documentation sparse. A full provider requires many storage callbacks, and their order and obligations are easier to reconstruct from docstrings and tests than from one end-to-end guide.
Maintenance4/5The unarchived repository was last pushed on 2026-07-14, GitHub reports 117 open issues and pull requests, and version 3.3.1 remains current on PyPI. The 3.3 series added device authorization server work, newer Python support, PKCE corrections, and a published security policy; 3.3.1 then repaired expiry parsing within days. Releases are less frequent than repository activity, but protocol and security work continues.
Ecosystem5/5The supplied snapshot records 71,797,955 weekly downloads, and GitHub shows 2,979 stars. Much of that use comes through wrappers: the project lists `requests-oauthlib`, Django OAuth Toolkit, django-allauth, Flask-Dance, Pyramid, and Bottle integrations. This indirect adoption proves the protocol layer fits many transports and frameworks, while also showing that raw oauthlib is rarely the shortest application-level route.

Discussed on

  1. hnShow HN: My first article: SSO using Flask and selenium8 points

Use it if

  • A custom HTTP transport needs OAuth request preparation or OAuth 1 signatures without coupling protocol code to `requests`.
  • An existing service still authenticates through OAuth 1 consumer and resource-owner credentials.
  • You are implementing an authorization server whose own storage controls clients, redirect URIs, grants, scopes, codes, and tokens.
  • Authorization code with PKCE, client credentials, refresh tokens, or device authorization is needed as a low-level protocol component.
Skip it if

Setup reality

We installed oauthlib 3.3.1 in a fresh Python 3.12 Bookworm sandbox. pip succeeded in 0.2 seconds, and the single installed package occupied 1 MB. pip-audit found 0 known vulnerabilities. Our inspection counted four direct dependency declarations, confirmed pure Python code and a BSD-3-Clause license, and found no py.typed marker. The package requires Python 3.8 or newer. import oauthlib completed in 0.11 seconds.

Client objects prepare request pieces; your transport must send the same HTTP method, URI, headers, and encoded body. Modifying an OAuth 1 query or form value after signing invalidates the signature. An authorization-code client needs server-side state generation, storage, and callback comparison. PKCE adds a verifier that must survive until token exchange, and create_code_challenge uses plain unless S256 is requested. Your application also owns token encryption, refresh timing, retry rules, and clock-skew handling.

Provider mode starts with a RequestValidator tied to your database. Its callbacks authenticate clients, compare registered redirects, limit scopes, save codes and bearer tokens, rotate refresh tokens, and associate users with validated requests. Several base methods raise NotImplementedError; others deny a request until overridden. Implement one grant at a time and test replayed codes, redirect mismatches, scope growth, revocation, and client-auth failures. Endpoint methods return (headers, body, status) for the framework adapter to preserve.

Normal OAuth calls require HTTPS. OAUTHLIB_INSECURE_TRANSPORT removes that check for local development, so never place it in shared production configuration. OAUTHLIB_RELAX_TOKEN_SCOPE accepts a provider returning a changed scope, which should trigger an application decision about gained or lost permissions. Version 3.3.1 repairs the expires_in regression from 3.3.0; avoid 3.3.0 anywhere refresh scheduling depends on parsed expiry. Missing py.typed also means strict type-checking behavior should be tested in your toolchain.

Patterns

Prepare an authorization-code redirect build-authorization-url

from oauthlib.oauth2 import WebApplicationClient

client = WebApplicationClient('client-id')
url = client.prepare_request_uri(
    'https://id.example/authorize',
    redirect_uri='https://app.example/callback',
    scope=['profile'],
    state=session_state,
)

Generate `state` with a cryptographic source, store it before redirect, and compare the callback value.

Parse the callback and check state validate-callback

client.parse_request_uri_response(
    callback_url,
    state=session_state,
)
authorization_code = client.code

A mismatch raises `MismatchingStateError`; swallowing it removes the request-forgery check.

Build the authorization-code form body prepare-token-exchange

body = client.prepare_request_body(
    code=authorization_code,
    redirect_uri='https://app.example/callback',
    client_secret=client_secret,
)
response = http.post(token_url, data=body, headers={
    'Content-Type': 'application/x-www-form-urlencoded',
})
token = client.parse_request_body_response(response.text)

The method returns an encoded form string. Preserve it and send the form content type.

Use an S256 PKCE challenge add-pkce

client = WebApplicationClient('public-client')
verifier = client.create_code_verifier(64)
challenge = client.create_code_challenge(verifier, 'S256')
url = client.prepare_request_uri(
    authorize_url, code_challenge=challenge, code_challenge_method='S256'
)
body = client.prepare_request_body(code=code, code_verifier=verifier)

Specify `S256` because the helper otherwise creates a plain challenge; retain the verifier until exchange.

Prepare a client-credentials grant request-client-credentials

from oauthlib.oauth2 import BackendApplicationClient

client = BackendApplicationClient(client_id='worker')
body = client.prepare_request_body(scope=['jobs:write'])
response = http.post(token_url, data=body, auth=('worker', secret))
token = client.parse_request_body_response(response.text)

The HTTP call performs client authentication here; oauthlib only prepares and parses the protocol body.

Add the current token to a request attach-bearer-token

uri, headers, body = client.add_token(
    'https://api.example/items',
    http_method='GET',
    headers={'Accept': 'application/json'},
)
response = http.get(uri, headers=headers)

`add_token` may raise `TokenExpiredError` without network access, leaving refresh policy to the caller.

Build a refresh-token request prepare-refresh

body = client.prepare_refresh_body(
    refresh_token=stored_token['refresh_token'],
    scope=['profile'],
)
response = http.post(token_url, data=body)
new_token = client.parse_request_body_response(response.text)

Replace the stored refresh token if the provider rotates it, or the next refresh may be rejected.

Sign an OAuth 1 request sign-oauth1-request

from oauthlib.oauth1 import Client, SIGNATURE_HMAC_SHA256

client = Client(
    consumer_key, client_secret=consumer_secret,
    resource_owner_key=access_token,
    resource_owner_secret=token_secret,
    signature_method=SIGNATURE_HMAC_SHA256,
)
uri, headers, body = client.sign(api_url, http_method='POST', body=form_body)

Send the returned URI, headers, and body unchanged because adding a parameter breaks the signature.

Start the device authorization grant prepare-device-request

from oauthlib.oauth2 import DeviceClient

client = DeviceClient('tv-client')
uri = client.prepare_request_uri(device_authorization_url, scope=['profile'])
response = http.post(uri)
device = response.json()

The application displays the verification URI and user code, then implements the provider-specified polling interval.

Validate registered redirect URIs implement-provider-validator

from oauthlib.oauth2 import RequestValidator

class Validator(RequestValidator):
    def validate_client_id(self, client_id, request, *args, **kwargs):
        return clients.exists(client_id)

    def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
        return redirect_uri in clients.redirect_uris(client_id)

Use exact registered redirect matching; prefix checks can deliver an authorization code to an attacker-controlled target.

Translate a provider token response create-token-response

headers, body, status = server.create_token_response(
    request_uri,
    http_method='POST',
    body=request_body,
    headers=request_headers,
    credentials={},
)
return FrameworkResponse(body, status=status, headers=headers)

Keep all three returned components intact when adapting them to your framework response.

Permit HTTP only in local development allow-local-http

import os

if settings.local_development:
    os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

This disables the secure-transport guard and belongs only behind an explicit local-development condition.

Alternatives

PackageRegistryPick it when
requests-oauthlibPyPIUse it when `requests` should send, sign, and refresh OAuth calls through a session.
authlibPyPIUse it for OAuth, OpenID Connect, JOSE, and framework integrations in one package family.
django-oauth-toolkitPyPIUse it for a Django OAuth 2 provider with models and Django REST Framework support.
pyjwtPyPIUse it when the job is only encoding and verifying JWTs rather than running an OAuth flow.

More security guides

cryptography · pyjwt · jose · requests-oauthlib · dompurify · jsonwebtoken · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.