Understanding OAuth 2.0

January 27, 20268 min read
OAuth 2.0 is an authorization framework that allows applications to access user data from other services without sharing passwords. Think "Sign in with Google" or "Connect with Facebook" - that's OAuth in action.

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

  1. Resource Owner - The user who owns the data
  2. Client - Your application that wants access
  3. Authorization Server - Issues access tokens (e.g., Google's auth server)
  4. 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

  1. 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
  2. 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

ProviderUse CaseDocumentation
GoogleGmail, Drive, CalendarGoogle OAuth
GitHubCode repositories, GistsGitHub OAuth
FacebookSocial login, PostsFacebook Login
MicrosoftOffice 365, AzureMicrosoft Identity
TwitterTweets, ProfileTwitter 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