OAuth Client Flows — Core Concepts
Why OAuth exists
Before OAuth, apps asked for your username and password directly. If you wanted a fitness app to read your health data from another service, you’d hand over your credentials. The fitness app could then do anything with your account — read your emails, change your settings, lock you out.
OAuth 2.0 solves this by introducing delegated authorization. The user authorizes specific permissions, and the app receives a token with only those permissions. No passwords are shared.
The four flows that matter
1. Authorization Code flow (most common)
Used by web applications where the server can keep secrets. This is the “full” OAuth flow:
- Your app redirects the user to the provider (Google, GitHub, etc.)
- The user logs in and approves the requested permissions
- The provider redirects back to your app with a temporary authorization code
- Your server exchanges the code for an access token (and often a refresh token)
The code-to-token exchange happens server-to-server, so the access token never appears in the browser’s URL bar.
2. Authorization Code + PKCE (for mobile and SPAs)
PKCE (Proof Key for Code Exchange) adds security when the client can’t keep a secret — like a mobile app or browser JavaScript. The client generates a random “code verifier” and sends a hash of it with the initial request. When exchanging the code for a token, it sends the original verifier. An attacker who intercepts the code can’t use it without the verifier.
This is now the recommended flow for all public clients and is increasingly recommended for confidential clients too.
3. Client Credentials flow (machine-to-machine)
No user involved. Your Python service authenticates directly with the provider using its client ID and secret. Used for backend services that access their own resources (not a user’s resources).
Example: a cron job that pulls analytics from a dashboard API, or a microservice calling another microservice.
4. Device Code flow (CLI tools and IoT)
For devices without browsers — CLI tools, smart TVs, IoT devices. The device shows a code and URL. The user opens the URL on their phone, enters the code, and approves. Meanwhile, the device polls the provider until the token is ready.
GitHub CLI (gh auth login) uses this flow.
Tokens explained
- Access token — short-lived (minutes to hours), attached to every API request. If stolen, damage is limited by its short lifespan.
- Refresh token — longer-lived, used only to get new access tokens. Stored securely, never sent to resource APIs.
- ID token — (OpenID Connect) contains user identity information (email, name). Not used for API access.
Token refresh: the silent renewal
Access tokens expire. When they do, the refresh flow kicks in:
- Client detects a
401 Unauthorizedresponse (or checks the token’sexpires_at) - Client sends the refresh token to the provider’s token endpoint
- Provider returns a new access token (and sometimes a new refresh token)
- Client retries the original request with the new token
Good Python OAuth clients handle this transparently — the caller never sees the expired token.
Scopes: limiting access
Scopes define what the token can do. When requesting authorization, you specify scopes like read:user, repo, or calendar.readonly. The token only grants access to those specific resources.
Requesting minimal scopes is a best practice. Don’t ask for admin access when you only need read. Users are more likely to approve limited permissions, and a compromised token causes less damage.
Common misconception
Many developers confuse OAuth (authorization) with OpenID Connect (authentication). OAuth tells you what an app is allowed to do. OpenID Connect tells you who the user is. They work together — OIDC is built on top of OAuth 2.0 — but they solve different problems. If you need login, use OIDC. If you need API access, use OAuth.
The one thing to remember: Pick the right OAuth flow for your Python client — Authorization Code + PKCE for apps with users, Client Credentials for machine-to-machine, and always handle token refresh transparently so callers never deal with expiry.
See Also
- Python Aiohttp Client Understand Aiohttp Client through a practical analogy so your Python decisions become faster and clearer.
- Python Api Client Design Why building your own API client in Python is like creating a TV remote that only has the buttons you actually need.
- Python Api Documentation Swagger Swagger turns your Python API into an interactive playground where anyone can click buttons to try it out — no coding required.
- Python Api Mocking Responses Why testing with fake API responses is like rehearsing a play with stand-ins before the real actors show up.
- Python Api Pagination Clients Why APIs send data in pages, and how Python handles it — like reading a book one chapter at a time instead of swallowing the whole thing.