Understanding OAuth 2.0
What Problem Does OAuth Solve?
Imagine you want to use a new photo printing app that needs access to your photos stored on Google Drive. Without OAuth, you'd have to give the app your Google password. That's dangerous!
OAuth lets you grant limited access to your resources without sharing credentials.
The Four Main Roles
- Resource Owner - The user who owns the data
- Client - Your application that wants access
- Authorization Server - Issues access tokens (e.g., Google's auth server)
- Resource Server - Hosts the protected resources (e.g., Google Drive API)
OAuth 2.0 Flow (Authorization Code)
This is the most secure and commonly used flow:
Python Implementation
Let's build a complete OAuth 2.0 implementation using Flask and GitHub as the provider:
Installation
pip install flask requests python-dotenv
Project Structure
oauth-demo/
āāā app.py
āāā .env
āāā templates/
āāā index.html
āāā profile.html
Main Application (app.py)
from flask import Flask, redirect, request, session, url_for, render_template_string
import requests
import secrets
import os
from urllib.parse import urlencode
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
# OAuth Configuration (GitHub example)
CLIENT_ID = "your_github_client_id"
CLIENT_SECRET = "your_github_client_secret"
REDIRECT_URI = "http://localhost:5000/callback"
# GitHub OAuth URLs
AUTHORIZATION_URL = "https://github.com/login/oauth/authorize"
TOKEN_URL = "https://github.com/login/oauth/access_token"
API_URL = "https://api.github.com/user"
class OAuthClient:
"""OAuth 2.0 Client Implementation"""
def __init__(self, client_id, client_secret, redirect_uri,
authorization_url, token_url, api_url):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.authorization_url = authorization_url
self.token_url = token_url
self.api_url = api_url
def get_authorization_url(self, state, scope="user"):
"""Generate authorization URL for user to visit"""
params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'scope': scope,
'state': state,
'response_type': 'code'
}
return f"{self.authorization_url}?{urlencode(params)}"
def exchange_code_for_token(self, code):
"""Exchange authorization code for access token"""
data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.redirect_uri
}
headers = {
'Accept': 'application/json'
}
response = requests.post(
self.token_url,
data=data,
headers=headers
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Token exchange failed: {response.text}")
def get_user_info(self, access_token):
"""Fetch user information using access token"""
headers = {
'Authorization': f'Bearer {access_token}',
'Accept': 'application/json'
}
response = requests.get(self.api_url, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"API request failed: {response.text}")
def refresh_access_token(self, refresh_token):
"""Refresh an expired access token"""
data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'refresh_token': refresh_token,
'grant_type': 'refresh_token'
}
response = requests.post(
self.token_url,
data=data,
headers={'Accept': 'application/json'}
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Token refresh failed: {response.text}")
# Initialize OAuth client
oauth_client = OAuthClient(
CLIENT_ID,
CLIENT_SECRET,
REDIRECT_URI,
AUTHORIZATION_URL,
TOKEN_URL,
API_URL
)
@app.route('/')
def index():
"""Home page with login button"""
template = """
<!DOCTYPE html>
<html>
<head>
<title>OAuth Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
text-align: center;
}
.login-btn {
background-color: #24292e;
color: white;
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.login-btn:hover {
background-color: #2f363d;
}
</style>
</head>
<body>
<h1>OAuth 2.0 Demo</h1>
<p>Click below to sign in with GitHub</p>
<a href="{{ url_for('login') }}" class="login-btn">
Sign in with GitHub
</a>
</body>
</html>
"""
return render_template_string(template)
@app.route('/login')
def login():
"""Step 1 & 2: Redirect user to authorization server"""
# Generate random state for CSRF protection
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
# Get authorization URL
auth_url = oauth_client.get_authorization_url(state)
print(f"Redirecting to: {auth_url}")
return redirect(auth_url)
@app.route('/callback')
def callback():
"""Step 5-7: Handle callback and exchange code for token"""
# Verify state to prevent CSRF attacks
state = request.args.get('state')
if state != session.get('oauth_state'):
return "Invalid state parameter", 400
# Check for errors
error = request.args.get('error')
if error:
return f"Authorization failed: {error}", 400
# Get authorization code
code = request.args.get('code')
if not code:
return "No authorization code received", 400
try:
# Exchange code for access token
token_data = oauth_client.exchange_code_for_token(code)
access_token = token_data.get('access_token')
# Store token in session (in production, use secure storage)
session['access_token'] = access_token
print(f"Access token received: {access_token[:20]}...")
# Redirect to profile page
return redirect(url_for('profile'))
except Exception as e:
return f"Error during token exchange: {str(e)}", 500
@app.route('/profile')
def profile():
"""Step 8-9: Fetch and display user data"""
access_token = session.get('access_token')
if not access_token:
return redirect(url_for('index'))
try:
# Fetch user information
user_info = oauth_client.get_user_info(access_token)
template = """
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.profile-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.profile-img {
border-radius: 50%;
width: 150px;
height: 150px;
}
.logout-btn {
background-color: #dc3545;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="profile-card">
<h1>Welcome!</h1>
<img src="{{ user_info.avatar_url }}" class="profile-img" alt="Avatar">
<h2>{{ user_info.name or user_info.login }}</h2>
<p><strong>Username:</strong> {{ user_info.login }}</p>
<p><strong>Bio:</strong> {{ user_info.bio or 'No bio available' }}</p>
<p><strong>Public Repos:</strong> {{ user_info.public_repos }}</p>
<p><strong>Followers:</strong> {{ user_info.followers }}</p>
<a href="{{ url_for('logout') }}" class="logout-btn">Logout</a>
</div>
</body>
</html>
"""
return render_template_string(template, user_info=user_info)
except Exception as e:
return f"Error fetching user info: {str(e)}", 500
@app.route('/logout')
def logout():
"""Clear session and logout"""
session.clear()
return redirect(url_for('index'))
if __name__ == '__main__':
print("OAuth Demo Server")
print("=" * 50)
print(f"Visit: http://localhost:5000")
print(f"Redirect URI: {REDIRECT_URI}")
print("=" * 50)
app.run(debug=True, port=5000)
Setting Up GitHub OAuth
-
Register your application:
- Go to GitHub Settings ā Developer settings ā OAuth Apps
- Click "New OAuth App"
- Set Authorization callback URL:
http://localhost:5000/callback - Get your Client ID and Client Secret
-
Update the code:
CLIENT_ID = "your_actual_client_id" CLIENT_SECRET = "your_actual_client_secret"
Running the Application
python app.py
Visit http://localhost:5000 and click "Sign in with GitHub"!
OAuth Grant Types
Token Lifecycle
Security Best Practices
1. Use State Parameter (CSRF Protection)
# Generate random state
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
# Verify on callback
if request.args.get('state') != session.get('oauth_state'):
return "CSRF attack detected", 403
2. Use HTTPS in Production
# Never use HTTP in production
REDIRECT_URI = "https://yourdomain.com/callback" # ā
Good
# REDIRECT_URI = "http://yourdomain.com/callback" # ā Bad
3. Secure Token Storage
from cryptography.fernet import Fernet
class TokenStorage:
def __init__(self):
self.cipher = Fernet(Fernet.generate_key())
def encrypt_token(self, token):
return self.cipher.encrypt(token.encode()).decode()
def decrypt_token(self, encrypted_token):
return self.cipher.decrypt(encrypted_token.encode()).decode()
4. Implement Token Refresh
def get_valid_token():
"""Get valid access token, refresh if expired"""
access_token = session.get('access_token')
expires_at = session.get('expires_at')
# Check if token is expired
if expires_at and time.time() > expires_at:
refresh_token = session.get('refresh_token')
token_data = oauth_client.refresh_access_token(refresh_token)
session['access_token'] = token_data['access_token']
session['expires_at'] = time.time() + token_data['expires_in']
return token_data['access_token']
return access_token
Common OAuth Providers
| Provider | Use Case | Documentation |
|---|---|---|
| Gmail, Drive, Calendar | Google OAuth | |
| GitHub | Code repositories, Gists | GitHub OAuth |
| Social login, Posts | Facebook Login | |
| Microsoft | Office 365, Azure | Microsoft Identity |
| Tweets, Profile | Twitter OAuth |
OAuth vs Other Auth Methods
Conclusion
OAuth 2.0 is the industry standard for secure authorization. It allows users to grant limited access to their resources without sharing passwords. The Python implementation above provides a solid foundation for integrating OAuth into your applications.
Key Takeaways:
- OAuth is for authorization, not authentication
- Use the Authorization Code flow for web applications
- Always implement state parameter for CSRF protection
- Store tokens securely and use HTTPS
- Refresh tokens when they expire