Authentication

Requests · Practical · 10 min

Nearly every real API requires credentials, and getting auth right keeps secrets safe.

The auth= argument attaches credentials to a request. Common schemes:

  • Basic auth: auth=("user", "pass"), a shortcut for HTTPBasicAuth. The credentials are only base64-encoded, so use it over HTTPS only.
  • Digest auth: HTTPDigestAuth("user", "pass"), rare today.
  • Bearer tokens (OAuth 2 access tokens, API keys): set the header Authorization: Bearer <token>, either per request or on a session's headers.
  • API keys in a custom header such as X-API-Key, or occasionally in the query string.

For anything custom, subclass requests.auth.AuthBase and implement __call__(self, r), which receives the PreparedRequest and returns it after modifying headers. This keeps auth logic in one place and lets you set session.auth = MyAuth(...). Request-signing schemes (HMAC signatures over the body) are written this way.

Requests also reads credentials from a ~/.netrc file automatically when no auth= is given. That surprises people when a header they set by hand is replaced, so be aware of it.

Security habits:

  • Load secrets from environment variables or a secret manager, not source code.
  • Requests strips the Authorization header when a redirect goes to a different host, so credentials do not leak to third parties.
  • Never log full request headers.

For OAuth 2 flows (getting and refreshing tokens), use a library such as requests-oauthlib or authlib rather than writing the flow by hand.

Example

import os
import requests
from requests.auth import AuthBase

r = requests.get("https://httpbin.org/basic-auth/ada/s3cret",
                 auth=("ada", "s3cret"), timeout=10)
print(r.status_code, r.json())   # 200 {'authenticated': True, 'user': 'ada'}

class BearerAuth(AuthBase):
    def __init__(self, token):
        self.token = token
    def __call__(self, r):       # r is the PreparedRequest
        r.headers["Authorization"] = f"Bearer {self.token}"
        return r

token = os.environ.get("API_TOKEN", "demo-token")
with requests.Session() as s:
    s.auth = BearerAuth(token)
    r = s.get("https://httpbin.org/bearer", timeout=10)
    print(r.json())              # {'authenticated': True, 'token': 'demo-token'}

print(requests.get("https://httpbin.org/bearer", timeout=10).status_code)  # 401

Remember

  • auth=(user, pass) for Basic; an Authorization header or AuthBase for tokens
  • AuthBase.__call__ modifies the prepared request; set it on a session
  • Load secrets from the environment; Authorization is dropped on cross-host redirects

Watch out for

  • Hard-coding API tokens in source code that ends up in version control.

Check yourself

  1. What must the __call__ method of a custom AuthBase return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: requests.readthedocs.io/en/latest/user/authentication/