Full Stack Learning Hub

Comprehensive guides, cheat sheets, and code examples for full stack development.

View on GitHub

API Authentication Guide

Quick Reference Card

Auth Method Use Case Implementation Example
API Key Simple public APIs Header/query param headers={'X-API-Key': 'key'}
Bearer Token Modern APIs Authorization header Authorization: Bearer token
Basic Auth Legacy/simple Username:password Base64 auth=('user', 'pass')
OAuth 2.0 Third-party access Complex flow Spotify, Google, GitHub
JWT Stateless auth Encoded token Login returns JWT
Session Auth Web apps Cookies/sessions Flask-Login

Common Headers:

Table of Contents

  1. Authentication Basics
  2. API Keys
  3. Bearer Tokens
  4. Basic Authentication
  5. OAuth 2.0
  6. JWT (JSON Web Tokens)
  7. Session-Based Authentication
  8. Best Practices
  9. Complete Examples

Authentication Basics

What is Authentication?

# Authentication - Verifying who you are
# "Are you really John Doe?"

# Authorization - What you're allowed to do
# "John Doe can view this resource"

# Together they secure APIs

Why Authenticate APIs?

# 1. Identify users
user = get_user_from_token(token)

# 2. Rate limiting
if user.requests_today > 1000:
    return "Rate limit exceeded", 429

# 3. Access control
if not user.has_permission('delete_users'):
    return "Forbidden", 403

# 4. Track usage
log_api_usage(user, endpoint)

API Keys

Simple API Key

import requests

API_KEY = 'your-api-key-here'

# Method 1: Header
response = requests.get(
    'https://api.example.com/data',
    headers={'X-API-Key': API_KEY}
)

# Method 2: Query parameter
response = requests.get(
    'https://api.example.com/data',
    params={'api_key': API_KEY}
)

print(response.json())

Implementing API Key Auth in Flask

from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)

# Store API keys (in production, use database)
VALID_API_KEYS = {
    'key123': 'user1',
    'key456': 'user2'
}

def require_api_key(f):
    """Decorator to require API key"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        api_key = request.headers.get('X-API-Key')

        if not api_key:
            return jsonify({'error': 'API key missing'}), 401

        if api_key not in VALID_API_KEYS:
            return jsonify({'error': 'Invalid API key'}), 401

        # Store user in request context
        request.user = VALID_API_KEYS[api_key]

        return f(*args, **kwargs)

    return decorated_function

@app.route('/api/data')
@require_api_key
def get_data():
    """Protected endpoint"""
    return jsonify({
        'message': f'Hello {request.user}!',
        'data': [1, 2, 3]
    })

if __name__ == '__main__':
    app.run(debug=True)

Using Environment Variables

import os
from dotenv import load_dotenv
import requests

# Load from .env file
load_dotenv()

API_KEY = os.getenv('API_KEY')

response = requests.get(
    'https://api.example.com/data',
    headers={'X-API-Key': API_KEY}
)

Bearer Tokens

Using Bearer Tokens

import requests

ACCESS_TOKEN = 'your-access-token'

# Bearer token in Authorization header
response = requests.get(
    'https://api.example.com/user/profile',
    headers={'Authorization': f'Bearer {ACCESS_TOKEN}'}
)

print(response.json())

Implementing Bearer Token Auth

from flask import Flask, request, jsonify
from functools import wraps
import secrets

app = Flask(__name__)

# Token storage (use database in production)
TOKENS = {}

def generate_token():
    """Generate secure random token"""
    return secrets.token_urlsafe(32)

def require_token(f):
    """Decorator to require bearer token"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        auth_header = request.headers.get('Authorization')

        if not auth_header:
            return jsonify({'error': 'Authorization header missing'}), 401

        try:
            scheme, token = auth_header.split()

            if scheme.lower() != 'bearer':
                return jsonify({'error': 'Invalid authentication scheme'}), 401

            if token not in TOKENS:
                return jsonify({'error': 'Invalid token'}), 401

            request.user = TOKENS[token]
            return f(*args, **kwargs)

        except ValueError:
            return jsonify({'error': 'Invalid authorization header'}), 401

    return decorated_function

@app.route('/api/login', methods=['POST'])
def login():
    """Login and get token"""
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    # Validate credentials (simplified)
    if username == 'admin' and password == 'password':
        token = generate_token()
        TOKENS[token] = username

        return jsonify({'access_token': token}), 200

    return jsonify({'error': 'Invalid credentials'}), 401

@app.route('/api/protected')
@require_token
def protected():
    """Protected endpoint"""
    return jsonify({'message': f'Hello {request.user}!'})

if __name__ == '__main__':
    app.run(debug=True)

Basic Authentication

Using Basic Auth

import requests

# Method 1: Using auth parameter
response = requests.get(
    'https://api.example.com/data',
    auth=('username', 'password')
)

# Method 2: Manual base64 encoding
import base64

credentials = base64.b64encode(b'username:password').decode('ascii')
response = requests.get(
    'https://api.example.com/data',
    headers={'Authorization': f'Basic {credentials}'}
)

print(response.json())

Implementing Basic Auth in Flask

from flask import Flask, request, jsonify
from functools import wraps
import base64

app = Flask(__name__)

# User database (simplified)
USERS = {
    'admin': 'password123',
    'user': 'pass456'
}

def check_auth(username, password):
    """Check if username/password is valid"""
    return USERS.get(username) == password

def require_basic_auth(f):
    """Decorator for basic auth"""
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization

        if not auth or not check_auth(auth.username, auth.password):
            return jsonify({'error': 'Authentication required'}), 401, {
                'WWW-Authenticate': 'Basic realm="Login Required"'
            }

        return f(*args, **kwargs)

    return decorated

@app.route('/api/data')
@require_basic_auth
def get_data():
    """Protected with basic auth"""
    return jsonify({'message': 'Success', 'data': [1, 2, 3]})

if __name__ == '__main__':
    app.run(debug=True)

OAuth 2.0

OAuth 2.0 Flow

"""
OAuth 2.0 Authorization Code Flow:

1. User clicks "Login with Spotify/Google/GitHub"
2. Redirect to provider's authorization URL
3. User grants permission
4. Provider redirects back with authorization code
5. Exchange code for access token
6. Use access token to make API calls
"""

Spotify OAuth Example

import requests
from flask import Flask, redirect, request, session
import os

app = Flask(__name__)
app.secret_key = 'your-secret-key'

# Spotify credentials
CLIENT_ID = os.getenv('SPOTIFY_CLIENT_ID')
CLIENT_SECRET = os.getenv('SPOTIFY_CLIENT_SECRET')
REDIRECT_URI = 'http://localhost:5000/callback'

# Spotify URLs
AUTH_URL = 'https://accounts.spotify.com/authorize'
TOKEN_URL = 'https://accounts.spotify.com/api/token'
API_BASE_URL = 'https://api.spotify.com/v1'

@app.route('/')
def index():
    """Home page with login button"""
    return '''
        <h1>Spotify OAuth Demo</h1>
        <a href="/login">Login with Spotify</a>
    '''

@app.route('/login')
def login():
    """Redirect to Spotify authorization"""
    scope = 'user-read-private user-read-email'

    params = {
        'client_id': CLIENT_ID,
        'response_type': 'code',
        'redirect_uri': REDIRECT_URI,
        'scope': scope
    }

    auth_url = f"{AUTH_URL}?{requests.compat.urlencode(params)}"
    return redirect(auth_url)

@app.route('/callback')
def callback():
    """Handle callback from Spotify"""
    code = request.args.get('code')

    # Exchange authorization code for access token
    token_data = {
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': REDIRECT_URI,
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET
    }

    response = requests.post(TOKEN_URL, data=token_data)
    token_info = response.json()

    # Store token in session
    session['access_token'] = token_info['access_token']

    return redirect('/profile')

@app.route('/profile')
def profile():
    """Get user profile from Spotify"""
    access_token = session.get('access_token')

    if not access_token:
        return redirect('/login')

    headers = {'Authorization': f'Bearer {access_token}'}
    response = requests.get(f'{API_BASE_URL}/me', headers=headers)

    if response.status_code == 200:
        user_data = response.json()
        return f'''
            <h1>Profile</h1>
            <p>Name: {user_data.get('display_name')}</p>
            <p>Email: {user_data.get('email')}</p>
            <p>Country: {user_data.get('country')}</p>
        '''

    return 'Error fetching profile', 400

if __name__ == '__main__':
    app.run(debug=True)

GitHub OAuth Example

import requests
from flask import Flask, redirect, request, session

app = Flask(__name__)
app.secret_key = 'your-secret-key'

# GitHub credentials
GITHUB_CLIENT_ID = 'your-client-id'
GITHUB_CLIENT_SECRET = 'your-client-secret'
REDIRECT_URI = 'http://localhost:5000/callback'

@app.route('/login')
def github_login():
    """Redirect to GitHub authorization"""
    params = {
        'client_id': GITHUB_CLIENT_ID,
        'redirect_uri': REDIRECT_URI,
        'scope': 'user:email'
    }

    url = f"https://github.com/login/oauth/authorize?{requests.compat.urlencode(params)}"
    return redirect(url)

@app.route('/callback')
def github_callback():
    """Handle GitHub callback"""
    code = request.args.get('code')

    # Exchange code for token
    token_data = {
        'client_id': GITHUB_CLIENT_ID,
        'client_secret': GITHUB_CLIENT_SECRET,
        'code': code
    }

    headers = {'Accept': 'application/json'}
    response = requests.post(
        'https://github.com/login/oauth/access_token',
        data=token_data,
        headers=headers
    )

    token_info = response.json()
    access_token = token_info['access_token']

    # Get user info
    headers = {'Authorization': f'Bearer {access_token}'}
    user_response = requests.get('https://api.github.com/user', headers=headers)

    user_data = user_response.json()
    return f"Hello, {user_data['login']}!"

if __name__ == '__main__':
    app.run(debug=True)

Real-World Example: Spotify Client Credentials Flow

This example demonstrates OAuth 2.0 Client Credentials Grant - used for server-to-server authentication without user login.

import requests
import base64
from datetime import datetime, timedelta

class SpotifyAPI:
    """
    Spotify API Client using OAuth 2.0 Client Credentials Flow

    This flow is used when:
    - Your app needs to access Spotify catalog data
    - No user-specific data is needed (no playlists, saved tracks, etc.)
    - Server-to-server authentication
    """

    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token = None
        self.token_expires_at = None
        self.token_url = "https://accounts.spotify.com/api/token"
        self.api_base_url = "https://api.spotify.com/v1"

    def get_token(self):
        """
        Get access token using Client Credentials Flow

        Steps:
        1. Base64 encode client_id:client_secret
        2. Send POST request with grant_type=client_credentials
        3. Receive access token (no refresh token in this flow)
        """

        # Step 1: Create authorization string
        # Format: client_id:client_secret -> Base64 encode
        auth_string = f"{self.client_id}:{self.client_secret}"
        auth_bytes = auth_string.encode('utf-8')
        auth_base64 = base64.b64encode(auth_bytes).decode('utf-8')

        # Step 2: Prepare request
        headers = {
            "Authorization": f"Basic {auth_base64}",
            "Content-Type": "application/x-www-form-urlencoded"
        }

        data = {
            "grant_type": "client_credentials"
        }

        # Step 3: Request token
        response = requests.post(self.token_url, headers=headers, data=data)

        if response.status_code == 200:
            token_data = response.json()
            self.access_token = token_data['access_token']

            # Calculate expiration time (tokens typically last 1 hour)
            expires_in = token_data.get('expires_in', 3600)
            self.token_expires_at = datetime.now() + timedelta(seconds=expires_in)

            print(f"Token acquired successfully")
            print(f"  Expires at: {self.token_expires_at.strftime('%H:%M:%S')}")
            return self.access_token
        else:
            print(f"Failed to get token: {response.status_code}")
            print(f"  Response: {response.json()}")
            return None

    def is_token_valid(self):
        """Check if current token is still valid"""
        if not self.access_token or not self.token_expires_at:
            return False

        # Token is valid if it hasn't expired (with 5 min buffer)
        return datetime.now() < (self.token_expires_at - timedelta(minutes=5))

    def ensure_token(self):
        """Ensure we have a valid token, refresh if needed"""
        if not self.is_token_valid():
            print("Token expired or missing, getting new token...")
            return self.get_token()
        return self.access_token

    def search_track(self, query):
        """
        Search for a track on Spotify

        Args:
            query: Search query (artist and/or song name)

        Returns:
            dict: First matching track or None
        """
        self.ensure_token()

        url = f"{self.api_base_url}/search"
        headers = {
            "Authorization": f"Bearer {self.access_token}"
        }
        params = {
            "q": query,
            "type": "track",
            "limit": 1
        }

        response = requests.get(url, headers=headers, params=params)

        if response.status_code == 200:
            data = response.json()
            tracks = data.get('tracks', {}).get('items', [])

            if tracks:
                track = tracks[0]
                return {
                    "name": track['name'],
                    "artist": track['artists'][0]['name'],
                    "album": track['album']['name'],
                    "preview_url": track.get('preview_url'),
                    "spotify_url": track['external_urls']['spotify']
                }
        elif response.status_code == 401:
            # Token expired, try one more time
            print("Token unauthorized, refreshing...")
            self.get_token()
            return self.search_track(query)  # Retry once

        return None

    def get_artist_top_tracks(self, artist_id, country='US'):
        """Get artist's top tracks"""
        self.ensure_token()

        url = f"{self.api_base_url}/artists/{artist_id}/top-tracks"
        headers = {
            "Authorization": f"Bearer {self.access_token}"
        }
        params = {
            "country": country
        }

        response = requests.get(url, headers=headers, params=params)

        if response.status_code == 200:
            data = response.json()
            tracks = []

            for track in data.get('tracks', [])[:5]:  # Top 5
                tracks.append({
                    "name": track['name'],
                    "popularity": track['popularity'],
                    "preview_url": track.get('preview_url')
                })

            return tracks

        return None


# Usage Example
if __name__ == "__main__":
    # Initialize with credentials (store these in environment variables!)
    from creds import client_id, client_secret

    spotify = SpotifyAPI(client_id, client_secret)

    # Get token
    spotify.get_token()

    # Search for a track
    print("\nSearching for 'Bohemian Rhapsody'...")
    track = spotify.search_track("Bohemian Rhapsody Queen")

    if track:
        print(f"\nFound: {track['name']}")
        print(f"   Artist: {track['artist']}")
        print(f"   Album: {track['album']}")
        print(f"   Spotify: {track['spotify_url']}")

    # Token is automatically reused for subsequent requests
    print("\nSearching for another track...")
    track2 = spotify.search_track("Stairway to Heaven Led Zeppelin")

    if track2:
        print(f"\nFound: {track2['name']}")
        print(f"   Artist: {track2['artist']}")

Key Differences from Authorization Code Flow:

Client Credentials Authorization Code
Server-to-server User authentication
No user login required Requires user to log in
No user-specific data Access user playlists, profile, etc.
Token lasts ~1 hour Has refresh token
Base64 encoded credentials Authorization code exchange
grant_type=client_credentials grant_type=authorization_code

Why This Example is Valuable:


JWT (JSON Web Tokens)

Understanding JWT

"""
JWT Structure: header.payload.signature

Header: Algorithm and token type
Payload: Claims (user data)
Signature: Verification
"""

Using PyJWT

import jwt
from datetime import datetime, timedelta

SECRET_KEY = 'your-secret-key'

def create_token(user_id, username):
    """Create JWT token"""
    payload = {
        'user_id': user_id,
        'username': username,
        'exp': datetime.utcnow() + timedelta(hours=24),  # Expires in 24 hours
        'iat': datetime.utcnow()  # Issued at
    }

    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

def verify_token(token):
    """Verify and decode JWT token"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return None  # Token expired
    except jwt.InvalidTokenError:
        return None  # Invalid token

# Usage
token = create_token(1, 'alice')
print(f"Token: {token}")

payload = verify_token(token)
if payload:
    print(f"User: {payload['username']}")
else:
    print("Invalid token")

# Install: pip install pyjwt

Flask JWT Authentication

from flask import Flask, request, jsonify
from functools import wraps
import jwt
from datetime import datetime, timedelta

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

# User database (simplified)
USERS = {
    'alice': {'id': 1, 'password': 'password123'},
    'bob': {'id': 2, 'password': 'pass456'}
}

def create_token(user_id, username):
    """Create JWT token"""
    payload = {
        'user_id': user_id,
        'username': username,
        'exp': datetime.utcnow() + timedelta(hours=24)
    }
    return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')

def token_required(f):
    """Decorator to require JWT token"""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')

        if not token:
            return jsonify({'error': 'Token missing'}), 401

        try:
            # Remove 'Bearer ' prefix if present
            if token.startswith('Bearer '):
                token = token[7:]

            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            request.user = data

        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Invalid token'}), 401

        return f(*args, **kwargs)

    return decorated

@app.route('/api/login', methods=['POST'])
def login():
    """Login and get JWT"""
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    user = USERS.get(username)

    if user and user['password'] == password:
        token = create_token(user['id'], username)
        return jsonify({'access_token': token}), 200

    return jsonify({'error': 'Invalid credentials'}), 401

@app.route('/api/profile')
@token_required
def profile():
    """Get user profile (protected)"""
    return jsonify({
        'user_id': request.user['user_id'],
        'username': request.user['username']
    })

if __name__ == '__main__':
    app.run(debug=True)

Using JWT in Requests

import requests

# Login to get token
response = requests.post(
    'http://localhost:5000/api/login',
    json={'username': 'alice', 'password': 'password123'}
)

token = response.json()['access_token']

# Use token for protected endpoints
headers = {'Authorization': f'Bearer {token}'}
response = requests.get(
    'http://localhost:5000/api/profile',
    headers=headers
)

print(response.json())

Session-Based Authentication

Flask-Login Example

from flask import Flask, request, jsonify, session
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required

app = Flask(__name__)
app.secret_key = 'your-secret-key'

# Initialize Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)

class User(UserMixin):
    """User model"""
    def __init__(self, id, username):
        self.id = id
        self.username = username

# User database
USERS = {
    '1': User('1', 'alice'),
    '2': User('2', 'bob')
}

@login_manager.user_loader
def load_user(user_id):
    """Load user by ID"""
    return USERS.get(user_id)

@app.route('/login', methods=['POST'])
def login():
    """Login endpoint"""
    data = request.get_json()
    username = data.get('username')

    # Find user (simplified)
    for user in USERS.values():
        if user.username == username:
            login_user(user)
            return jsonify({'message': 'Logged in successfully'})

    return jsonify({'error': 'User not found'}), 404

@app.route('/logout')
@login_required
def logout():
    """Logout endpoint"""
    logout_user()
    return jsonify({'message': 'Logged out'})

@app.route('/profile')
@login_required
def profile():
    """Protected endpoint"""
    from flask_login import current_user
    return jsonify({'username': current_user.username})

# Install: pip install flask-login

Best Practices

1. Never Hardcode Credentials

# Bad
API_KEY = 'sk-1234567890abcdef'

# Good
import os
API_KEY = os.getenv('API_KEY')

2. Use HTTPS

# All authentication should use HTTPS
# HTTP sends credentials in plain text!

# Good
BASE_URL = 'https://api.example.com'

# Bad (for production)
BASE_URL = 'http://api.example.com'

3. Set Token Expiration

# Tokens should expire
payload = {
    'user_id': 1,
    'exp': datetime.utcnow() + timedelta(hours=24)  # Expires in 24h
}

4. Store Tokens Securely

# Client-side: Use httpOnly cookies or secure storage
# Server-side: Hash/encrypt sensitive data
# Never store in localStorage for sensitive apps

5. Implement Rate Limiting

from flask_limiter import Limiter

limiter = Limiter(app, key_func=lambda: request.headers.get('X-API-Key'))

@app.route('/api/data')
@limiter.limit("100 per day")
def get_data():
    return jsonify({'data': [1, 2, 3]})

# Install: pip install Flask-Limiter

6. Validate Tokens Properly

def verify_token(token):
    """Always validate tokens"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])

        # Check expiration
        if datetime.utcnow() > datetime.fromtimestamp(payload['exp']):
            return None

        return payload

    except jwt.InvalidTokenError:
        return None

Complete Examples

Complete JWT API

#!/usr/bin/env python3
"""
Complete JWT Authentication API
"""

from flask import Flask, request, jsonify
from functools import wraps
import jwt
from datetime import datetime, timedelta
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-change-in-production'

# User database (use real database in production)
USERS = {}

def create_token(user_id):
    """Create JWT token"""
    payload = {
        'user_id': user_id,
        'exp': datetime.utcnow() + timedelta(days=1),
        'iat': datetime.utcnow()
    }
    return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')

def token_required(f):
    """Decorator for protected routes"""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None

        if 'Authorization' in request.headers:
            auth_header = request.headers['Authorization']
            try:
                token = auth_header.split(" ")[1]
            except IndexError:
                return jsonify({'error': 'Bearer token malformed'}), 401

        if not token:
            return jsonify({'error': 'Token is missing'}), 401

        try:
            data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            current_user = USERS.get(data['user_id'])

            if not current_user:
                return jsonify({'error': 'User not found'}), 401

        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token has expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Token is invalid'}), 401

        return f(current_user, *args, **kwargs)

    return decorated

@app.route('/api/register', methods=['POST'])
def register():
    """Register new user"""
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    if not username or not password:
        return jsonify({'error': 'Username and password required'}), 400

    if username in [u['username'] for u in USERS.values()]:
        return jsonify({'error': 'Username already exists'}), 400

    user_id = len(USERS) + 1
    USERS[user_id] = {
        'id': user_id,
        'username': username,
        'password': generate_password_hash(password)
    }

    return jsonify({'message': 'User created successfully'}), 201

@app.route('/api/login', methods=['POST'])
def login():
    """Login and get token"""
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    for user in USERS.values():
        if user['username'] == username:
            if check_password_hash(user['password'], password):
                token = create_token(user['id'])
                return jsonify({'access_token': token}), 200

    return jsonify({'error': 'Invalid credentials'}), 401

@app.route('/api/profile')
@token_required
def profile(current_user):
    """Get user profile"""
    return jsonify({
        'id': current_user['id'],
        'username': current_user['username']
    })

@app.route('/api/protected')
@token_required
def protected(current_user):
    """Protected endpoint"""
    return jsonify({'message': f'Hello, {current_user["username"]}!'})

if __name__ == '__main__':
    app.run(debug=True)

See Also