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

Sponsored

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:read or email. 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.

  1. Client initiates: Your app redirects the user to the authorization server's /authorize endpoint with parameters like response_type=code, client_id, redirect_uri, and scope.
  2. User authenticates: The user logs in at the authorization server (username/password, 2FA, whatever).
  3. 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_uri with a code query parameter.
  4. Client exchanges code: The app's backend sends the code, along with its client_id and client_secret, to the token endpoint.
  5. Tokens issued: The authorization server validates everything and returns an access_token, optionally a refresh_token, and metadata like expires_in.
  6. API call: The app includes the access token in the Authorization: Bearer <token> header when calling the resource server.
  7. 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 the scope parameter and re-authorize.
  • state parameter omitted: This invites CSRF attacks. Always generate a random state value 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 Authorization header.
  • 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 state parameter: 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

  1. Authorization Code with PKCE: Ideal for public clients (SPAs/mobile). Uses a code_verifier and challenge to prevent interception without a client secret.
  2. Client Credentials: For machine-to-machine communication (service accounts). No user involvement; use for backend-to-backend APIs.
  3. 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 state parameter 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.