OAuth 2.0 Core Concepts
Understand OAuth 2.0 core concepts in this first lesson of the OAuth 2 · OpenID Connect track. Learn the problem it solves, the mental model, and how it works step by step.
Focus: understand oauth 2.0 core concepts
You've built a sleek new web app. Users love it. But now you need to let them sign in with Google, or let a third-party service access their data. The naive approach — asking for their username and password — is a security nightmare waiting to happen. That's the exact pain point OAuth 2.0 solves: a delegated authorization framework that lets users grant limited access to their resources without ever sharing credentials. In this first lesson of the OAuth 2 · OpenID Connect track, you'll grasp the core concepts that make OAuth tick, from roles and tokens to the authorization code flow, and you'll be ready to tackle the next lessons on token stewardship and real-world flows.
The problem this lesson solves
Think about the last time an app asked for your Google or GitHub password so it could "sync your contacts." If you're a developer, you know that's a terrible idea. Sharing passwords means:
- Full access: The app gets everything, not just the contacts.
- No revocation: You can't take back access without changing your password everywhere.
- Credential leakage: Every integration becomes a potential point of compromise.
OAuth 2.0 emerged to fix this exact problem. It's a delegation protocol: instead of handing over your password, the resource server (like Google) issues a token — a limited, revocable, time-boxed credential that grants the app access to only what you approved. As a developer, understanding OAuth 2.0 core concepts is non-negotiable because almost every modern auth system — from social logins to API gateways to microservices — runs on this protocol.
Core concept / mental model
Let's demystify OAuth 2.0 with a simple analogy: a hotel key card.
You walk into a hotel. You don't get the master key to every room. Instead, the front desk (the authorization server) gives you a key card (the access token) that only opens your room (the resource) for a limited time (the expiration). If you lose it, the desk can cancel it — you don't need to replace all the locks.
In OAuth terms, there are four roles:
- Resource Owner: The user who owns the data (e.g., your contacts).
- Resource Server: The API that holds the data (e.g., Google Contacts API).
- Client: The app requesting access (e.g., your web app).
- Authorization Server: The service that authenticates the user and issues tokens (e.g., Google's OAuth endpoint).
Pro tip: Confused about authentication vs authorization? Authentication is "who are you?" — OAuth is not built for that. Authorization is "what can you do?" — that's OAuth's job. OpenID Connect (later in this track) adds authentication on top of OAuth.
Key terms you'll see everywhere
- Access Token: A short-lived credential (often a JWT or opaque string) sent to the resource server. It grants access to specific scopes.
- Refresh Token: A longer-lived credential that lets the client get new access tokens without user interaction.
- Scope: A permission, like
contacts:readoremail. The user sees and approves each scope. - Authorization Code: A temporary one-time code exchanged for an access token in the authorization code flow.
Visualize the flow (in words):
User (Resource Owner)
|
| 1. "Log in with Google"
v
Client (Your App) ------> Authorization Server (Google)
| |
| 2. Redirects user | 3. User authenticates
| | and approves scope
| |
|<----------------------|
| 4. Authorization code |
v
Client exchanges code + client credentials for tokens
|
v
Resource Server (API) accepts access token
How it works step by step
Let's trace the most common OAuth 2.0 flow — the authorization code flow — from user click to API call.
- Client initiates: Your app redirects the user to the authorization server's
/authorizeendpoint with parameters likeresponse_type=code,client_id,redirect_uri, andscope. - User authenticates: The user logs in at the authorization server (username/password, 2FA, whatever).
- User approves: The user sees a consent screen describing exactly what the app wants (read contacts? write to drive?). If approved, the authorization server redirects back to the client's
redirect_uriwith acodequery parameter. - Client exchanges code: The app's backend sends the code, along with its
client_idandclient_secret, to the token endpoint. - Tokens issued: The authorization server validates everything and returns an
access_token, optionally arefresh_token, and metadata likeexpires_in. - API call: The app includes the access token in the
Authorization: Bearer <token>header when calling the resource server. - Resource server validates: The API checks the token's signature, expiry, and scopes, then returns the data.
Why the extra code step? The code itself is exchanged for tokens only in a secure back-channel (server-to-server), so the token never touches the browser. This mitigates token theft from malicious scripts.
Hands-on walkthrough
Let's make this concrete. We'll simulate the authorization code flow using Python and requests. For this demo, we'll use a mock authorization server (you can spin up a local one with flask or use a public test server like OAuth Playground). Here's a minimal client that fetches a token:
import requests
from urllib.parse import urlencode
# 1. Build the authorize URL (you'd get client_id from your app registration)
auth_url = "https://mock-auth-server.com/authorize"
params = {
"response_type": "code",
"client_id": "your-client-id",
"redirect_uri": "https://yourapp.com/callback",
"scope": "contacts:read",
"state": "random-csrf-token", # prevent CSRF, more below
}
print("Redirect user to:", f"{auth_url}?{urlencode(params)}")
# In real life, the user gets redirected here with ?code=...
# Assume we captured the code from the query string:
authorization_code = "abc123"
# 2. Exchange the code for tokens
token_url = "https://mock-auth-server.com/token"
payload = {
"grant_type": "authorization_code",
"code": authorization_code,
"redirect_uri": "https://yourapp.com/callback",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
}
resp = requests.post(token_url, data=payload)
tokens = resp.json()
print("Access token:", tokens.get("access_token"))
print("Expires in:", tokens.get("expires_in"))
# 3. Use the access token on the resource server
resource_resp = requests.get(
"https://mock-api.com/contacts",
headers={"Authorization": f"Bearer {tokens['access_token']}"}
)
print("Contacts:", resource_resp.json())
Expected output (mock):
Redirect user to: https://mock-auth-server.com/authorize?response_type=code&client_id=your-client-id&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback&scope=contacts%3Aread&state=random-csrf-token
Access token: eyJhbGciOiJIUzI1NiIs...
Expires in: 3600
Contacts: [{'id': 1, 'name': 'Alice'}]
Now, the refresh flow: Access tokens live short (an hour). To avoid bothering the user again, you use the refresh token:
refresh_payload = {
"grant_type": "refresh_token",
"refresh_token": tokens["refresh_token"],
"client_id": "your-client-id",
"client_secret": "your-client-secret",
}
new_tokens = requests.post(token_url, data=refresh_payload).json()
print("New access token:", new_tokens["access_token"])
Compare options / when to choose what
OAuth 2.0 defines several grant types (flows). Choosing the right one depends on your client type and security needs.
| Grant Type | Best for | Pros | Cons |
|---|---|---|---|
| Authorization Code | Web apps with a backend | Most secure; token never in browser | Requires server-side component |
| PKCE (Proof Key for Code Exchange) | SPAs, mobile apps | Protects against code interception; no client secret needed | Adds complexity |
| Client Credentials | Machine-to-machine (service accounts) | Simple, no user interaction | No user context |
| Implicit (deprecated) | Legacy single-page apps | Was simple | Token exposed in URL; do not use for new apps |
| Resource Owner Password (deprecated) | Trusted first-party apps | Direct login | Needs user password; not recommended |
Pro tip: For modern single-page apps, always use the Authorization Code flow with PKCE — never the implicit flow. Browsers can't keep secrets, and PKCE adds a cryptographic challenge that secures the exchange.
Troubleshooting & edge cases
- Invalid
redirect_uri: The authorization server will reject the request if the redirect URI doesn't exactly match the one registered. Fix: copy the registered URI character-for-character (watch for trailing slashes). - Expired access token (401): If the resource server returns
401 Unauthorized, your token expired or was revoked. Use the refresh token to get a new one. - Missing
scope: If the API returns insufficient scope, you didn't request or were denied that scope during consent. Check thescopeparameter and re-authorize. stateparameter omitted: This invites CSRF attacks. Always generate a randomstatevalue and verify it on the callback. Here's a simple check:
import secrets
# On redirect to auth server:
state = secrets.token_urlsafe(16)
session["oauth_state"] = state
# On callback:
if request.args.get("state") != session.get("oauth_state"):
abort(400, "State mismatch — possible CSRF")
- Token in URL (anti-pattern): Never put access tokens in query strings — they leak via logs and history. Always use the
Authorizationheader. - Clock skew: If you validate JWT exp/iat locally, ensure your server clock is synced (use NTP) to avoid false expirations.
What you learned & what's next
You now understand OAuth 2.0 core concepts: the problem of credential sharing, the hotel-key-card mental model, the four roles, and the step-by-step authorization code flow. You also exercised the flow with a real Python client and learned when to choose which grant type — plus common pitfalls like the state parameter and token expiry.
What's next? In the next lesson, you'll dive into token stewardship — how to securely store, refresh, and validate tokens in production. With OAuth under your belt, you'll be ready to tackle OpenID Connect to add authentication on top. Keep this foundational knowledge; every flow you build on this track will use these same building blocks.
Remember: OAuth is a framework, not a one-size-fits-all solution. The decisions you make — which flow, how you handle
state, how you store refresh tokens — define your security posture.
Practice recap
Open your favorite OAuth playground (or use a mock server) and run through the authorization code flow manually with Python: build the authorize URL, capture the code, exchange it, and call an API. Then modify the exercise to include a state parameter and verify it on the callback. Pay attention to the token response fields — you'll need them in the next lesson on token stewardship.
Common mistakes
- Treating OAuth as authentication: OAuth grants access, it doesn't verify identity. Combine with OpenID Connect (ID token) for authentication.
- Omitting the
stateparameter: This opens a CSRF vector. Always generate and validate a random state on the callback. - Using the implicit flow for new apps: The implicit flow exposes tokens in URLs and is deprecated. Choose Authorization Code with PKCE for SPAs and mobile apps.
- Storing access tokens in localStorage: This allows XSS to steal tokens. Keep tokens in memory or secure cookies instead.
- Ignoring token expiration: Forgetting to handle 401s with a refresh token leads to broken user sessions. Implement a robust token renewal strategy.
Variations
- Authorization Code with PKCE: Ideal for public clients (SPAs/mobile). Uses a code_verifier and challenge to prevent interception without a client secret.
- Client Credentials: For machine-to-machine communication (service accounts). No user involvement; use for backend-to-backend APIs.
- OpenID Connect (the next lesson): Adds an ID token (JWT) for authentication, giving you user claims like email and name on top of OAuth authorization.
Real-world use cases
- Adding 'Log in with Google' to a web app — users grant email/profile access without sharing passwords.
- Integrating a third-party CRM that reads your users' calendar events after consent.
- A microservice backend using client credentials to call a payment API on behalf of the application itself.
Key takeaways
- OAuth 2.0 is a delegated authorization framework that eliminates password sharing by issuing scoped, revocable tokens.
- The four OAuth roles are resource owner, resource server, client, and authorization server — each with a clear job.
- The authorization code flow is the gold standard for web apps; always use PKCE for SPAs and mobile.
- Access tokens expire fast; refresh tokens let you get new ones securely without user interaction.
- Always use the
stateparameter to prevent CSRF and never put tokens in URLs. - Choose the grant type based on your client type: auth code for web, PKCE for SPA/mobile, client credentials for machine-to-machine.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.