OAuth 2.0 — Complete Authorization Framework Guide
In this tutorial, you will learn about OAuth 2.0. We cover key concepts, practical examples, and best practices to help you master this topic.
OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service, allowing users to grant third-party access without sharing their credentials.
What You'll Learn
By the end of this lesson, you will implement the OAuth 2.0 authorization code flow with PKCE, understand all four grant types, configure scopes for fine-grained permissions, and build an OAuth 2.0 client and server.
Why It Matters
OAuth 2.0 is the industry standard for delegated authorization, used by Google, Facebook, GitHub, and every major platform. Doda Browser uses OAuth 2.0 to allow users to grant the browser access to their cloud storage without sharing passwords. Durga Antivirus Pro uses OAuth 2.0 for its enterprise SSO integration.
Real-World Use
A user wants to print photos from an online printing service. Instead of giving the printing service their Google password, the user clicks "Sign in with Google." Google asks if the printing service can access their photos. The user approves, and the printing service receives a limited access token for photo printing only.
OAuth 2.0 Flow
sequenceDiagram
participant User
participant App as Third-Party App
participant Auth as Authorization Server
participant API as Resource Server
User->>App: Click "Login with Provider"
App->>Auth: Authorization Request (client_id, redirect_uri, scope)
Auth->>User: Authenticate & Grant Permissions
User->>Auth: Approve
Auth->>App: Authorization Code (via redirect)
App->>Auth: Exchange Code + Client Secret for Tokens
Auth-->>App: Access Token + Refresh Token
App->>API: API Request with Access Token
API-->>App: Protected Resource
Authorization Code Flow with PKCE
import requests
import secrets
import hashlib
import base64
class OAuth2Client:
def __init__(self, client_id, client_secret, redirect_uri, token_url):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.token_url = token_url
def generate_pkce_pair(self):
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip("=").decode()
return code_verifier, code_challenge
def get_authorization_url(self, auth_url, scope="openid profile"):
_, code_challenge = self.generate_pkce_pair()
params = {
"response_type": "code",
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"scope": scope,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"state": secrets.token_urlsafe(16),
}
url = f"{auth_url}?{'&'.join(f'{k}={v}' for k, v in params.items())}"
print(f"[OAuth2] Authorization URL generated")
return url
def exchange_code(self, code, code_verifier):
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": self.redirect_uri,
"client_id": self.client_id,
"client_secret": self.client_secret,
"code_verifier": code_verifier,
}
response = requests.post(self.token_url, data=data, timeout=10)
response.raise_for_status()
tokens = response.json()
print(f"[OAuth2] Access token received: {tokens.get('access_token', '')[:20]}...")
return tokens
def refresh_access_token(self, refresh_token):
data = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": self.client_id,
"client_secret": self.client_secret,
}
response = requests.post(self.token_url, data=data, timeout=10)
if response.status_code == 200:
return response.json()
return None
OAuth 2.0 Grant Types
| Grant Type | Use Case | Security |
|---|---|---|
| Authorization Code | Server-side web apps, mobile apps | Highest (with PKCE) |
| Client Credentials | Server-to-server, Microservices | High (no user involved) |
| Device Code | Smart TVs, CLI tools, IoT | Medium |
| Refresh Token | Prolonging access without re-auth | High (with rotation) |
Client Credentials Grant
// Service-to-service OAuth 2.0
const axios = require("axios");
async function getClientCredentialsToken() {
const response = await axios.post("https://auth.example.com/token", {
grant_type: "client_credentials",
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
scope: "api:read api:write",
}, {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
const { access_token, expires_in } = response.data;
console.log(`Service token: ${access_token.substring(0, 20)}...`);
console.log(`Expires in: ${expires_in}s`);
return access_token;
}
async function callProtectedAPI() {
const token = await getClientCredentialsToken();
const result = await axios.get("https://api.example.com/orders", {
headers: { Authorization: `Bearer ${token}` },
});
return result.data;
}
Common Mistakes
- Not using PKCE for mobile and SPA clients, leaving them vulnerable to authorization code interception.
- Exposing client_secret in mobile apps or SPAs where it cannot be kept confidential.
- Using overly broad scopes instead of requesting the minimum permissions needed.
- Not validating the redirect_uri against a strict allowlist allows open redirect attacks.
- Failing to validate the state parameter exposes the flow to CSRF Attacks on the redirect.
- Using the implicit grant type (now deprecated) which exposes tokens in the URL fragment.
Practice Questions
- Why is PKCE required for mobile apps using OAuth 2.0?
Mobile apps cannot securely store a client secret. PKCE uses a dynamically generated code verifier that proves the app requesting the token is the same app that started the authorization flow.
- What is the purpose of the state parameter in OAuth 2.0?
The state parameter prevents CSRF attacks on the authorization callback. The app generates a random state value, includes it in the authorization request, and verifies it matches when the callback is received.
- When should you use the client credentials grant?
For server-to-server communication where no user is involved. Examples: a cron job fetching data from an API, a microservice authenticating to another microservice, or a backend service calling an admin API.
- Challenge: Build a complete OAuth 2.0 authorization server that supports authorization code flow with PKCE, client credentials grant, refresh tokens, scoped access, and token introspection endpoint.
FAQ
Mini Project: OAuth 2.0 Token Exchange CLI
Build a CLI tool that performs the OAuth 2.0 authorization code flow with PKCE, stores tokens securely, and provides a command to call protected APIs with automatic token refresh.
import os
import json
import requests
import secrets
import hashlib
import base64
from pathlib import Path
class OAuth2CLI:
def __init__(self, config_path=".oauth_config.json"):
self.config_path = Path(config_path)
self.token_path = Path(".oauth_token.json")
self.config = self._load_config()
def _load_config(self):
if self.config_path.exists():
return json.loads(self.config_path.read_text())
return {}
def configure(self, client_id, auth_url, token_url, redirect_uri):
config = {
"client_id": client_id,
"auth_url": auth_url,
"token_url": token_url,
"redirect_uri": redirect_uri,
}
self.config_path.write_text(json.dumps(config, indent=2))
print(f"Configuration saved to {self.config_path}")
def login(self):
if not self.config:
print("Run configure first")
return
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip("=").decode()
auth_url = f"{self.config['auth_url']}?response_type=code&client_id={self.config['client_id']}&redirect_uri={self.config['redirect_uri']}&code_challenge={challenge}&code_challenge_method=S256&state={secrets.token_urlsafe(16)}"
print(f"Open in browser:\n{auth_url}\n")
code = input("Paste authorization code: ").strip()
response = requests.post(self.config["token_url"], data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": self.config["redirect_uri"],
"client_id": self.config["client_id"],
"code_verifier": verifier,
})
tokens = response.json()
self.token_path.write_text(json.dumps(tokens, indent=2))
print("Token stored")
def get_token(self):
tokens = json.loads(self.token_path.read_text())
return tokens.get("access_token")
What's Next
Learn about OpenID Connect for authentication on top of OAuth 2.0, then explore SAML authentication for enterprise SSO.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro