API Security
Modern applications increasingly rely on APIs (Application Programming Interfaces) to enable communication between services, mobile apps, and third-party integrations. While APIs provide powerful functionality and enable modern architectures like microservices, they also introduce significant security challenges. API vulnerabilities can lead to data breaches, unauthorized access, and complete system compromise.
This lecture covers the critical security considerations for REST APIs, GraphQL, authentication mechanisms, and the OWASP API Security Top 10.
Why API Security Matters
The API Security Crisis:
83% of web traffic is API traffic (Cloudflare, 2024)
69% of API services vulnerable to DoS attacks (2024)
APIs are 200% more vulnerable than web applications to attacks
API attacks increased 400% from 2022 to 2024
Over 60% of enterprises will use GraphQL by 2027
Real-World Breaches:
T-Mobile (2023): API vulnerability exposed 37 million customer records
Optus (2022): API misconfiguration leaked 9.8 million Australian records
Peloton (2021): BOLA vulnerability exposed all user data
Facebook (2019): API flaw exposed 50 million accounts
Why APIs Are Targeted:
Direct Database Access: APIs often connect directly to databases
Authentication Complexity: Token-based auth introduces new attack vectors
Insufficient Testing: APIs often lack the same security rigor as web UIs
Documentation Exposure: API docs reveal attack surface
Third-Party Integration: External APIs introduce supply chain risks
Rate Limiting Gaps: Resource exhaustion easier than traditional web apps
REST API Security
REST (Representational State Transfer) is the most common API architectural style, using HTTP methods for CRUD operations. REST APIs present unique security challenges compared to traditional web applications.
REST API Fundamentals
Key Characteristics:
Stateless: Each request contains all necessary information
Resource-Based: Operations on resources (e.g.,
/users/123)HTTP Methods: GET, POST, PUT, PATCH, DELETE
Standard Status Codes: 200, 401, 403, 404, 500, etc.
Content Negotiation: JSON, XML response formats
Typical REST Endpoint:
GET /api/v1/users/123 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/jsonResponse:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"username": "alice",
"email": "[email protected]",
"role": "user"
}Common REST API Vulnerabilities
1. Broken Object Level Authorization (BOLA/IDOR)
Definition: API fails to validate that the authenticated user has permission to access the requested object.
Example Vulnerable Endpoint:
@app.route('/api/users/<user_id>/profile')
@require_authentication
def get_user_profile(user_id):
# VULNERABLE: No authorization check
user = User.query.get(user_id)
return jsonify({
'id': user.id,
'username': user.username,
'email': user.email,
'ssn': user.ssn, # Sensitive data!
'salary': user.salary
})Attack:
# Attacker is authenticated as user ID 456
GET /api/users/123/profile HTTP/1.1
Authorization: Bearer <attacker_token>
# Response: Full profile of user 123 (unauthorized access!)Prevention:
@app.route('/api/users/<user_id>/profile')
@require_authentication
def get_user_profile(user_id):
user = User.query.get(user_id)
# Check authorization
if user.id != current_user.id and not current_user.is_admin:
return jsonify({'error': 'Forbidden'}), 403
return jsonify({
'id': user.id,
'username': user.username,
'email': user.email
})Automated BOLA Testing:
# Use different authorization tokens for same resource
curl -H "Authorization: Bearer USER_A_TOKEN" https://api.example.com/api/users/123
curl -H "Authorization: Bearer USER_B_TOKEN" https://api.example.com/api/users/123
# Should fail for USER_B2. Excessive Data Exposure
Definition: API returns more data than necessary, exposing sensitive information.
Vulnerable Code:
// Node.js/Express
app.get('/api/users', async (req, res) => {
// VULNERABLE: Returns entire user object
const users = await User.find({});
res.json(users);
});
// Database user object contains:
// { id, username, email, password_hash, ssn, credit_card, internal_notes }Attack Response:
{
"users": [
{
"id": 1,
"username": "alice",
"email": "[email protected]",
"password_hash": "$2b$10$...",
"ssn": "123-45-6789",
"credit_card": "4532-****-****-1234",
"internal_notes": "VIP customer, waive fees"
}
]
}Prevention (Data Transfer Objects):
// Define DTO (Data Transfer Object)
class UserDTO {
constructor(user) {
this.id = user.id;
this.username = user.username;
this.email = user.email;
// Explicitly exclude sensitive fields
}
}
app.get('/api/users', async (req, res) => {
const users = await User.find({});
// Map to DTOs
const userDTOs = users.map(u => new UserDTO(u));
res.json(userDTOs);
});3. Mass Assignment
Definition: API automatically binds client input to internal object properties without filtering, allowing attackers to modify unintended fields.
Vulnerable Code:
# Flask endpoint
@app.route('/api/users/<user_id>', methods=['PUT'])
@require_authentication
def update_user(user_id):
user = User.query.get(user_id)
if user.id != current_user.id:
return jsonify({'error': 'Forbidden'}), 403
# VULNERABLE: Blindly updates all fields from request
for key, value in request.json.items():
setattr(user, key, value)
db.session.commit()
return jsonify({'message': 'Updated'})Attack Payload:
PUT /api/users/123 HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token>
{
"username": "alice",
"email": "[email protected]",
"is_admin": true,
"account_balance": 1000000
}Result: Attacker gains admin privileges and arbitrary account balance!
Prevention (Whitelist Fields):
# Define allowed fields
ALLOWED_FIELDS = {'username', 'email', 'bio', 'avatar_url'}
@app.route('/api/users/<user_id>', methods=['PUT'])
@require_authentication
def update_user(user_id):
user = User.query.get(user_id)
if user.id != current_user.id:
return jsonify({'error': 'Forbidden'}), 403
# Only update whitelisted fields
for key, value in request.json.items():
if key in ALLOWED_FIELDS:
setattr(user, key, value)
db.session.commit()
return jsonify({'message': 'Updated'})4. Lack of Rate Limiting
Definition: API doesn't limit the number of requests, enabling brute force, DoS, and resource exhaustion attacks.
Attack Scenarios:
# Brute force API key
for key in $(cat wordlist.txt); do
curl -H "X-API-Key: $key" https://api.example.com/data
done
# Password brute force
for pass in $(cat passwords.txt); do
curl -d '{"username":"admin","password":"'$pass'"}' \
https://api.example.com/login
done
# Resource exhaustion
while true; do
curl https://api.example.com/expensive-operation &
donePrevention:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute") # Only 5 login attempts per minute
def login():
# Login logic
pass
@app.route('/api/expensive-operation')
@limiter.limit("10 per hour")
def expensive_operation():
# Resource-intensive operation
passAdvanced Rate Limiting (Token Bucket):
import time
from functools import wraps
class TokenBucket:
def __init__(self, capacity, refill_rate):
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate
self.last_refill = time.time()
def consume(self, tokens=1):
self._refill()
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False
def _refill(self):
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(self.capacity,
self.tokens + elapsed * self.refill_rate)
self.last_refill = now
# Usage
buckets = {}
def rate_limit(capacity, refill_rate):
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
client_id = get_client_identifier()
if client_id not in buckets:
buckets[client_id] = TokenBucket(capacity, refill_rate)
if not buckets[client_id].consume():
return jsonify({'error': 'Rate limit exceeded'}), 429
return f(*args, **kwargs)
return wrapped
return decorator
@app.route('/api/data')
@rate_limit(capacity=100, refill_rate=10) # 100 tokens, refill 10/second
def get_data():
return jsonify({'data': 'sensitive information'})5. API Authentication Bypass
Common Weaknesses:
a) Predictable API Keys:
# VULNERABLE: Sequential API keys
def generate_api_key(user_id):
return f"api_key_{user_id}_{int(time.time())}"
# Attacker can predict keys:
# api_key_123_1699999999
# api_key_124_1699999999b) Missing Authentication on Endpoints:
// VULNERABLE: Forgot to add authentication
app.get('/api/admin/users', (req, res) => {
// No authentication check!
const users = await User.find({});
res.json(users);
});c) JWT Algorithm Confusion:
import jwt
# VULNERABLE: Accepts 'none' algorithm
def verify_token(token):
# Missing algorithm specification
payload = jwt.decode(token, SECRET_KEY, options={"verify_signature": False})
return payload
# Attacker creates token with alg=nonePrevention:
# Secure API key generation
import secrets
def generate_api_key():
return secrets.token_urlsafe(32) # Cryptographically secure random key
# Always require authentication
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key or not validate_api_key(api_key):
return jsonify({'error': 'Invalid API key'}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/api/admin/users')
@require_api_key
def get_users():
return jsonify(users)
# Secure JWT validation
def verify_token(token):
try:
# Explicitly specify allowed algorithms
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.InvalidTokenError:
return NoneREST API Security Best Practices
1. API Versioning
https://api.example.com/v1/users
https://api.example.com/v2/usersBenefits:
Deprecate insecure endpoints gradually
Maintain backward compatibility
Clear security policy per version
2. Input Validation
from marshmallow import Schema, fields, ValidationError
class UserCreateSchema(Schema):
username = fields.Str(required=True, validate=lambda x: len(x) >= 3)
email = fields.Email(required=True)
age = fields.Int(validate=lambda x: 0 < x < 150)
@app.route('/api/users', methods=['POST'])
def create_user():
schema = UserCreateSchema()
try:
# Validate input
data = schema.load(request.json)
except ValidationError as err:
return jsonify({'errors': err.messages}), 400
# Create user with validated data
user = User(**data)
db.session.add(user)
db.session.commit()
return jsonify({'id': user.id}), 2013. Security Headers for APIs
@app.after_request
def set_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
# CORS configuration
response.headers['Access-Control-Allow-Origin'] = 'https://trusted-app.com'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response4. Logging and Monitoring
import logging
logger = logging.getLogger(__name__)
@app.route('/api/sensitive-operation')
@require_authentication
def sensitive_operation():
# Log security-relevant events
logger.info(f"User {current_user.id} accessed sensitive operation from {request.remote_addr}")
# Detect anomalies
if is_suspicious_activity(current_user, request):
logger.warning(f"Suspicious activity detected: {current_user.id}")
alert_security_team()
return jsonify({'status': 'success'})GraphQL Security
GraphQL is a query language for APIs that allows clients to request exactly the data they need. While this flexibility is powerful, it introduces unique security challenges not present in REST APIs.
GraphQL Fundamentals
Basic Query:
query {
user(id: 123) {
username
email
posts {
title
content
}
}
}Key Differences from REST:
Single Endpoint: All queries go to
/graphqlClient-Specified Queries: Client controls data structure
Introspection: Schema is discoverable
Flexible Relationships: Deep nested queries possible
GraphQL Vulnerabilities
1. Introspection Abuse
Definition: GraphQL exposes its entire schema through introspection, revealing all available queries, mutations, and data types.
Introspection Query:
query {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}Response:
{
"data": {
"__schema": {
"types": [
{
"name": "User",
"fields": [
{"name": "id", "type": {"name": "ID"}},
{"name": "username", "type": {"name": "String"}},
{"name": "email", "type": {"name": "String"}},
{"name": "ssn", "type": {"name": "String"}},
{"name": "creditCard", "type": {"name": "String"}},
{"name": "isAdmin", "type": {"name": "Boolean"}}
]
}
]
}
}
}Risk: Attacker learns entire API structure, including sensitive fields.
Prevention:
// Apollo Server - Disable introspection in production
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production'
});2. Batching Attacks (Authentication Bypass)
Definition: GraphQL allows multiple queries in a single request, enabling brute-force attacks that bypass rate limiting.
Attack Payload:
[
{ "query": "mutation { login(username: \"admin\", password: \"password1\") { token } }" },
{ "query": "mutation { login(username: \"admin\", password: \"password2\") { token } }" },
{ "query": "mutation { login(username: \"admin\", password: \"password3\") { token } }" },
...
{ "query": "mutation { login(username: \"admin\", password: \"password10000\") { token } }" }
]Result: 10,000 login attempts in a single HTTP request, bypassing traditional rate limiting.
Prevention:
// graphql-armor protection
const { ApolloArmor } = require('@escape.tech/graphql-armor');
const armor = new ApolloArmor({
maxDepth: {
enabled: true,
n: 10 // Max query depth
},
costLimit: {
enabled: true,
maxCost: 5000 // Max query cost
},
batching: {
enabled: true,
maxBatchSize: 5 // Limit batched queries
}
});
const server = new ApolloServer({
...armor.protect(),
typeDefs,
resolvers
});3. Query Depth / Complexity DoS
Definition: Deeply nested queries exhaust server resources, causing denial of service.
Attack Query:
query {
user(id: 1) {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
posts {
comments {
author {
friends {
# ... continues recursively
}
}
}
}
}
}
}
}
}
}
}
}
}
}Impact: Exponential database queries, server crash, DoS.
Prevention:
// Query depth limiting
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)] // Max depth: 10
});
// Query complexity analysis
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const ComplexityLimitRule = createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log('Query cost:', cost);
},
formatErrorMessage: (cost) => {
return `Query is too complex: ${cost}. Maximum allowed complexity: 1000`;
}
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [ComplexityLimitRule]
});4. Authorization Bypass in Resolvers
Vulnerable Resolver:
const resolvers = {
Query: {
// VULNERABLE: No authorization check
user: (parent, { id }, context) => {
return db.User.findById(id);
},
// VULNERABLE: Returns all users regardless of permissions
users: () => {
return db.User.findAll();
}
},
User: {
// VULNERABLE: Exposes sensitive field to everyone
ssn: (user) => {
return user.ssn;
}
}
};Secure Resolver:
const resolvers = {
Query: {
user: (parent, { id }, context) => {
const { currentUser } = context;
// Check authentication
if (!currentUser) {
throw new AuthenticationError('Not authenticated');
}
const user = db.User.findById(id);
// Check authorization
if (user.id !== currentUser.id && !currentUser.isAdmin) {
throw new ForbiddenError('Not authorized');
}
return user;
}
},
User: {
// Field-level authorization
ssn: (user, args, context) => {
const { currentUser } = context;
// Only user themselves or admin can see SSN
if (user.id !== currentUser.id && !currentUser.isAdmin) {
return null; // or throw ForbiddenError
}
return user.ssn;
}
}
};5. Injection in GraphQL
SQL Injection via GraphQL:
// VULNERABLE resolver
const resolvers = {
Query: {
searchUsers: (parent, { query }) => {
// Direct SQL concatenation
const sql = `SELECT * FROM users WHERE username LIKE '%${query}%'`;
return db.query(sql);
}
}
};Attack Query:
query {
searchUsers(query: "' OR '1'='1") {
username
email
}
}Prevention:
// Use parameterized queries
const resolvers = {
Query: {
searchUsers: (parent, { query }) => {
// Safe: Using ORM or parameterized query
return db.User.findAll({
where: {
username: {
[Op.like]: `%${query}%`
}
}
});
}
}
};GraphQL Security Best Practices
// Comprehensive GraphQL security setup
const { ApolloServer } = require('apollo-server');
const { ApolloArmor } = require('@escape.tech/graphql-armor');
const depthLimit = require('graphql-depth-limit');
const armor = new ApolloArmor({
maxDepth: { enabled: true, n: 10 },
costLimit: { enabled: true, maxCost: 5000 },
batching: { enabled: true, maxBatchSize: 5 },
characterLimit: { enabled: true, maxLength: 15000 }
});
const server = new ApolloServer({
typeDefs,
resolvers,
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
// Apply security rules
...armor.protect(),
validationRules: [depthLimit(10)],
// Authentication context
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const currentUser = await authenticateToken(token);
return { currentUser };
},
// Error handling (don't expose internals)
formatError: (error) => {
console.error(error);
if (process.env.NODE_ENV === 'production') {
return new Error('Internal server error');
}
return error;
}
});JWT Security
JSON Web Tokens (JWT) are the most common authentication mechanism for modern APIs. However, improper implementation can lead to critical vulnerabilities.
JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cDecoded Structure:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "user"
}
// Signature (HMAC-SHA256)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)JWT Vulnerabilities
1. None Algorithm Attack
Definition: JWT "none" algorithm allows unsigned tokens.
Vulnerable Code:
import jwt
def verify_token(token):
# VULNERABLE: Doesn't specify algorithms
payload = jwt.decode(token, SECRET_KEY, options={"verify_signature": False})
return payloadAttack:
import base64
import json
# Create token with alg=none
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "123", "role": "admin"}
token = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
token += '.'
token += base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
token += '.' # Empty signature
# Use token to gain admin accessPrevention:
import jwt
def verify_token(token):
try:
# Explicitly whitelist algorithms
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256', 'RS256'])
return payload
except jwt.InvalidTokenError:
return None2. Weak Secret Keys
Vulnerable:
# WEAK: Easily cracked
SECRET_KEY = "secret"
SECRET_KEY = "123456"
SECRET_KEY = "myapp"Cracking Weak JWT:
# Using hashcat
hashcat -m 16500 jwt.txt wordlist.txt
# Using jwt_tool
python3 jwt_tool.py <JWT> -C -d wordlist.txtPrevention:
import secrets
# Generate strong secret key (256 bits)
SECRET_KEY = secrets.token_urlsafe(32)
# Store in environment variable, never in code
SECRET_KEY = os.getenv('JWT_SECRET_KEY')3. Missing or Excessive Expiration
Vulnerable:
# No expiration - token valid forever!
payload = {
'sub': user.id,
'username': user.username
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')Attack: Stolen token usable indefinitely.
Prevention:
import datetime
def create_token(user):
payload = {
'sub': user.id,
'username': user.username,
'iat': datetime.datetime.utcnow(), # Issued at
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1) # Expires in 1 hour
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
# Validate expiration
def verify_token(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# jwt.decode automatically validates exp claim
return payload
except jwt.ExpiredSignatureError:
return None # Token expired
except jwt.InvalidTokenError:
return None4. Algorithm Confusion (HS256 vs RS256)
Definition: Attacker changes algorithm from RS256 (asymmetric) to HS256 (symmetric), using public key as HMAC secret.
Scenario:
# Server expects RS256 (RSA public/private key)
PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
"""
# VULNERABLE: Accepts any algorithm
def verify_token(token):
payload = jwt.decode(token, PUBLIC_KEY) # No algorithm specified
return payloadAttack:
import jwt
# Attacker creates token with HS256 using public key as secret
payload = {"sub": "123", "role": "admin"}
malicious_token = jwt.encode(payload, PUBLIC_KEY, algorithm='HS256')
# Server validates with public key, thinking it's HMAC secretPrevention:
# Always specify expected algorithm
def verify_token(token):
try:
payload = jwt.decode(token, PUBLIC_KEY, algorithms=['RS256'])
return payload
except jwt.InvalidTokenError:
return None5. Token Storage (Client-Side)
Vulnerable:
// localStorage is vulnerable to XSS
localStorage.setItem('token', jwtToken);
// Later...
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});Attack: Any XSS vulnerability can steal token from localStorage.
Better Approach:
// Use HttpOnly cookies (not accessible via JavaScript)
// Server sets cookie:
res.cookie('token', jwtToken, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});
// Browser automatically sends cookie with requests
fetch('/api/data', {
credentials: 'include' // Include cookies
});JWT Best Practices
import jwt
import datetime
import secrets
class JWTManager:
def __init__(self):
self.secret = os.getenv('JWT_SECRET_KEY')
self.algorithm = 'HS256'
self.token_expiry = datetime.timedelta(hours=1)
self.refresh_expiry = datetime.timedelta(days=30)
def create_access_token(self, user):
payload = {
'sub': str(user.id),
'username': user.username,
'role': user.role,
'type': 'access',
'iat': datetime.datetime.utcnow(),
'exp': datetime.datetime.utcnow() + self.token_expiry,
'jti': secrets.token_urlsafe(16) # Unique token ID
}
return jwt.encode(payload, self.secret, algorithm=self.algorithm)
def create_refresh_token(self, user):
payload = {
'sub': str(user.id),
'type': 'refresh',
'iat': datetime.datetime.utcnow(),
'exp': datetime.datetime.utcnow() + self.refresh_expiry,
'jti': secrets.token_urlsafe(16)
}
return jwt.encode(payload, self.secret, algorithm=self.algorithm)
def verify_token(self, token, token_type='access'):
try:
payload = jwt.decode(
token,
self.secret,
algorithms=[self.algorithm],
options={
'verify_exp': True,
'verify_iat': True,
'require': ['exp', 'iat', 'sub', 'type']
}
)
# Verify token type
if payload.get('type') != token_type:
return None
# Check if token is revoked (check against blacklist/database)
if self.is_token_revoked(payload.get('jti')):
return None
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
def is_token_revoked(self, jti):
# Check Redis or database for revoked tokens
return redis_client.exists(f'revoked_token:{jti}')
def revoke_token(self, jti, exp):
# Store in Redis with expiration
ttl = exp - datetime.datetime.utcnow()
redis_client.setex(f'revoked_token:{jti}', int(ttl.total_seconds()), '1')OAuth 2.0 & OpenID Connect
OAuth 2.0 is an authorization framework that enables third-party applications to obtain limited access to a service. OpenID Connect (OIDC) builds on OAuth 2.0 to add authentication.
OAuth 2.0 Flows
1. Authorization Code Flow (Most Secure)
Use Case: Web applications with backend server
Flow:
1. User → Client App: "Login with Google"
2. Client App → Authorization Server: Redirect with client_id, redirect_uri, scope
3. User → Authorization Server: Authenticates and grants permission
4. Authorization Server → Client App: Authorization code
5. Client App → Authorization Server: Exchange code for access token
6. Authorization Server → Client App: Access token
7. Client App → Resource Server: Request with access token
8. Resource Server → Client App: Protected resourceImplementation:
from flask import Flask, redirect, request, session
import requests
app = Flask(__name__)
OAUTH_CONFIG = {
'client_id': 'your_client_id',
'client_secret': 'your_client_secret',
'authorization_endpoint': 'https://oauth.example.com/authorize',
'token_endpoint': 'https://oauth.example.com/token',
'redirect_uri': 'https://yourapp.com/callback'
}
@app.route('/login')
def login():
# Step 1: Redirect to authorization endpoint
auth_url = (
f"{OAUTH_CONFIG['authorization_endpoint']}"
f"?response_type=code"
f"&client_id={OAUTH_CONFIG['client_id']}"
f"&redirect_uri={OAUTH_CONFIG['redirect_uri']}"
f"&scope=openid profile email"
f"&state={generate_state_token()}" # CSRF protection
)
return redirect(auth_url)
@app.route('/callback')
def callback():
# Step 2: Receive authorization code
code = request.args.get('code')
state = request.args.get('state')
# Validate state token (CSRF protection)
if not validate_state(state):
return 'Invalid state', 400
# Step 3: Exchange code for access token
token_response = requests.post(
OAUTH_CONFIG['token_endpoint'],
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': OAUTH_CONFIG['redirect_uri'],
'client_id': OAUTH_CONFIG['client_id'],
'client_secret': OAUTH_CONFIG['client_secret']
}
)
tokens = token_response.json()
access_token = tokens['access_token']
id_token = tokens.get('id_token') # OIDC
# Step 4: Use access token to get user info
user_info = requests.get(
'https://oauth.example.com/userinfo',
headers={'Authorization': f'Bearer {access_token}'}
).json()
# Create session
session['user_id'] = user_info['sub']
return redirect('/dashboard')OAuth 2.0 Vulnerabilities
1. Redirect URI Manipulation
Vulnerable Configuration:
# VULNERABLE: Substring matching
ALLOWED_REDIRECT_URIS = ['https://app.example.com']
def validate_redirect_uri(redirect_uri):
for allowed in ALLOWED_REDIRECT_URIS:
if allowed in redirect_uri: # WRONG!
return True
return False
# Attack redirect_uri:
# https://evil.com?url=https://app.example.com
# Passes validation!Attack:
https://oauth.example.com/authorize
?client_id=legitimate_app
&redirect_uri=https://attacker.com?url=https://app.example.com
&response_type=codeResult: Authorization code sent to attacker's server.
Prevention:
# Exact match only
ALLOWED_REDIRECT_URIS = [
'https://app.example.com/callback',
'https://app.example.com/oauth/callback'
]
def validate_redirect_uri(redirect_uri):
return redirect_uri in ALLOWED_REDIRECT_URIS2. State Parameter Missing (CSRF)
Vulnerable Flow:
@app.route('/login')
def login():
# VULNERABLE: No state parameter
auth_url = (
f"{AUTH_ENDPOINT}?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
)
return redirect(auth_url)
@app.route('/callback')
def callback():
code = request.args.get('code')
# VULNERABLE: No state validation
# Exchange code for token...Attack:
Attacker initiates OAuth flow, gets authorization code
Attacker tricks victim into visiting:
https://app.com/callback?code=ATTACKER_CODEVictim's account linked to attacker's OAuth account
Prevention:
import secrets
@app.route('/login')
def login():
# Generate CSRF token
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
auth_url = (
f"{AUTH_ENDPOINT}?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&state={state}" # Include state
)
return redirect(auth_url)
@app.route('/callback')
def callback():
code = request.args.get('code')
state = request.args.get('state')
# Validate state
if state != session.get('oauth_state'):
return 'Invalid state', 400
# Clear state
session.pop('oauth_state', None)
# Exchange code for token...3. Implicit Flow (Deprecated, Insecure)
Vulnerable:
# Access token in URL fragment (bad!)
https://app.com/callback#access_token=eyJhbGci...&token_type=BearerIssues:
Token exposed in browser history
Token in URL can leak via Referer header
No client authentication
Prevention: Use Authorization Code Flow with PKCE instead.
4. Open Redirect via OAuth
Scenario:
@app.route('/callback')
def callback():
# ... get access token ...
# VULNERABLE: Unvalidated redirect
next_url = request.args.get('next', '/')
return redirect(next_url)
# Attack:
# /callback?code=...&next=https://evil.comOAuth 2.0 Best Practices
# Complete secure OAuth 2.0 implementation
from flask import Flask, redirect, request, session, abort
import requests
import secrets
import time
class OAuth2Client:
def __init__(self, config):
self.config = config
self.session_key = 'oauth_state'
def generate_auth_url(self):
# Generate state for CSRF protection
state = secrets.token_urlsafe(32)
session[self.session_key] = {
'state': state,
'timestamp': time.time()
}
# Use PKCE for additional security
code_verifier = secrets.token_urlsafe(64)
code_challenge = self._generate_code_challenge(code_verifier)
session['code_verifier'] = code_verifier
params = {
'response_type': 'code',
'client_id': self.config['client_id'],
'redirect_uri': self.config['redirect_uri'],
'scope': self.config['scope'],
'state': state,
'code_challenge': code_challenge,
'code_challenge_method': 'S256'
}
from urllib.parse import urlencode
return f"{self.config['authorization_endpoint']}?{urlencode(params)}"
def handle_callback(self, code, state):
# Validate state
if not self._validate_state(state):
abort(400, 'Invalid state')
# Exchange code for token
code_verifier = session.pop('code_verifier', None)
token_data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': self.config['redirect_uri'],
'client_id': self.config['client_id'],
'client_secret': self.config['client_secret'],
'code_verifier': code_verifier
}
response = requests.post(
self.config['token_endpoint'],
data=token_data,
timeout=10
)
if response.status_code != 200:
abort(400, 'Token exchange failed')
return response.json()
def _validate_state(self, state):
saved_state = session.pop(self.session_key, None)
if not saved_state:
return False
# Check state matches
if saved_state['state'] != state:
return False
# Check state not expired (5 minutes)
if time.time() - saved_state['timestamp'] > 300:
return False
return True
def _generate_code_challenge(self, verifier):
import hashlib
import base64
digest = hashlib.sha256(verifier.encode()).digest()
return base64.urlsafe_b64encode(digest).decode().rstrip('=')OWASP API Security Top 10:2023
API1:2023 - Broken Object Level Authorization
Covered extensively in REST API section above.
API2:2023 - Broken Authentication
Examples:
Weak password requirements
No rate limiting on authentication endpoints
Missing MFA
Predictable session tokens
Prevention: See JWT and OAuth sections above.
API3:2023 - Broken Object Property Level Authorization
Example:
# User can update their profile
@app.route('/api/users/<user_id>', methods=['PUT'])
def update_user(user_id):
if user_id != current_user.id:
abort(403)
# VULNERABLE: User can set any property
for key, value in request.json.items():
setattr(current_user, key, value)
db.session.commit()
# Attack: {"is_admin": true, "balance": 9999999}Prevention: Whitelist allowed fields (mass assignment prevention shown earlier).
API4:2023 - Unrestricted Resource Consumption
Prevention: Rate limiting, resource quotas, pagination.
@app.route('/api/search')
@limiter.limit("100 per hour")
def search():
# Enforce pagination
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 20, type=int), 100) # Max 100
results = Model.query.paginate(page=page, per_page=per_page)
return jsonify(results)API5:2023 - Broken Function Level Authorization
Example:
# VULNERABLE: Admin function without authorization check
@app.route('/api/admin/delete-user/<user_id>', methods=['DELETE'])
def delete_user(user_id):
# Missing authorization check!
User.query.filter_by(id=user_id).delete()
db.session.commit()
return jsonify({'status': 'deleted'})Prevention:
def require_admin(f):
@wraps(f)
def decorated(*args, **kwargs):
if not current_user.is_admin:
abort(403, 'Admin access required')
return f(*args, **kwargs)
return decorated
@app.route('/api/admin/delete-user/<user_id>', methods=['DELETE'])
@require_authentication
@require_admin
def delete_user(user_id):
User.query.filter_by(id=user_id).delete()
db.session.commit()
return jsonify({'status': 'deleted'})API6:2023 - Unrestricted Access to Sensitive Business Flows
Example: Automated ticket purchasing bots
Prevention:
# CAPTCHA for sensitive operations
@app.route('/api/tickets/purchase', methods=['POST'])
@require_authentication
@limiter.limit("5 per hour")
def purchase_ticket():
# Verify CAPTCHA
if not verify_captcha(request.json.get('captcha_token')):
abort(400, 'Invalid CAPTCHA')
# Behavioral analysis
if is_suspicious_behavior(current_user):
abort(429, 'Suspicious activity detected')
# Process purchase...API7:2023 - Server-Side Request Forgery
Covered in Lecture 4 - Server-Side Vulnerabilities
API8:2023 - Security Misconfiguration
Examples:
Default credentials
Verbose error messages
CORS misconfiguration
Missing security headers
Prevention:
# Secure configuration
app.config['DEBUG'] = False
app.config['TESTING'] = False
@app.errorhandler(Exception)
def handle_error(error):
# Don't expose stack traces
if app.config['DEBUG']:
return str(error), 500
return 'Internal server error', 500
@app.after_request
def set_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Strict-Transport-Security'] = 'max-age=31536000'
# Restrictive CORS
allowed_origins = ['https://app.example.com']
origin = request.headers.get('Origin')
if origin in allowed_origins:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return responseAPI9:2023 - Improper Inventory Management
Issue: Unknown/undocumented API endpoints
Prevention:
Maintain API documentation (OpenAPI/Swagger)
Version APIs properly
Decommission old API versions
Regular API endpoint audits
# OpenAPI specification
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/api/users:
get:
summary: List users
security:
- bearerAuth: []
responses:
'200':
description: SuccessAPI10:2023 - Unsafe Consumption of APIs
Issue: Trusting third-party API responses without validation
Example:
# VULNERABLE: Trust external API blindly
def get_user_data(user_id):
response = requests.get(f'https://external-api.com/users/{user_id}')
data = response.json()
# Directly use without validation
User.query.filter_by(id=user_id).update(data)
db.session.commit()Prevention:
from marshmallow import Schema, fields, ValidationError
class ExternalUserSchema(Schema):
username = fields.Str(required=True, validate=lambda x: len(x) <= 50)
email = fields.Email(required=True)
age = fields.Int(validate=lambda x: 0 < x < 150)
def get_user_data(user_id):
try:
response = requests.get(
f'https://external-api.com/users/{user_id}',
timeout=5 # Timeout
)
response.raise_for_status()
data = response.json()
# Validate external data
schema = ExternalUserSchema()
validated_data = schema.load(data)
# Only update whitelisted fields
User.query.filter_by(id=user_id).update({
'username': validated_data['username'],
'email': validated_data['email']
})
db.session.commit()
except requests.RequestException:
# Handle API failures gracefully
pass
except ValidationError as err:
# Handle validation errors
passAPI Security Testing
Tools
1. Burp Suite:
Intercept API requests
Test for BOLA/IDOR
Automated scanning (Pro)
2. Postman:
API testing and automation
Collection runner for fuzzing
Pre-request scripts for auth
3. OWASP ZAP:
Free alternative to Burp
API scanning
Automation via CLI
4. Custom Scripts:
# BOLA/IDOR testing script
import requests
def test_bola(base_url, token_a, token_b):
"""Test if user A can access user B's resources"""
for user_id in range(1, 1000):
# Request with user A's token
response_a = requests.get(
f'{base_url}/api/users/{user_id}/profile',
headers={'Authorization': f'Bearer {token_a}'}
)
if response_a.status_code == 200:
# Try accessing same resource with user B's token
response_b = requests.get(
f'{base_url}/api/users/{user_id}/profile',
headers={'Authorization': f'Bearer {token_b}'}
)
if response_b.status_code == 200:
print(f'BOLA found! User B can access user {user_id}')
# Rate limiting test
def test_rate_limiting(url, headers):
"""Test if endpoint has rate limiting"""
for i in range(1000):
response = requests.post(url, headers=headers)
if response.status_code == 429:
print(f'Rate limit hit after {i} requests')
return
print('WARNING: No rate limiting detected after 1000 requests!')Key Takeaways
API Security is Critical: 83% of web traffic is API traffic; vulnerabilities lead to massive breaches
BOLA is #1 Threat: Always validate authorization at the object level, not just authentication
GraphQL Requires Special Attention: Introspection, batching, and query complexity attacks are unique to GraphQL
JWT Must Be Implemented Correctly: Algorithm confusion, weak secrets, and missing expiration are common pitfalls
OAuth 2.0 Needs CSRF Protection: Always use state parameter and validate redirect URIs exactly
Rate Limiting is Essential: APIs are susceptible to brute force and DoS without proper rate limits
Validate All Data: Never trust external APIs or client input; always validate and sanitize
Defense in Depth: Combine authentication, authorization, rate limiting, input validation, and monitoring
Keep APIs Documented: Maintain an accurate inventory of all API endpoints and versions
Test Continuously: Use automated tools and manual testing to identify API vulnerabilities
Hands-On Exercises
REST API Security Lab:
Deploy a vulnerable REST API (e.g., crAPI, VAmPI)
Test for BOLA/IDOR vulnerabilities
Exploit mass assignment
Implement fixes and retest
GraphQL Exploitation:
Set up DVGA (Damn Vulnerable GraphQL Application)
Perform introspection to map schema
Execute batching attack for brute force
Craft deeply nested query for DoS
Implement protections (depth limiting, cost analysis)
JWT Attacks:
Create vulnerable JWT implementation
Test none algorithm attack
Crack weak JWT secret
Perform algorithm confusion attack
Implement secure JWT handling
OAuth 2.0 Security:
Build OAuth 2.0 authorization server
Test redirect URI manipulation
Exploit missing state parameter (CSRF)
Implement PKCE flow
API Security Assessment:
Choose a public API or bug bounty target
Test OWASP API Top 10 vulnerabilities
Document findings in professional report
Submit responsible disclosure if vulnerabilities found
Resources
Official Documentation
OWASP API Security Top 10:2023: https://owasp.org/API-Security/editions/2023/en/0x11-t10/
GraphQL Security: https://graphql.org/learn/security/
JWT Best Practices: https://tools.ietf.org/html/rfc8725
OAuth 2.0 Security: https://tools.ietf.org/html/rfc6749
OAuth 2.0 Threat Model: https://tools.ietf.org/html/rfc6819
Practice Labs
PortSwigger API Testing Labs: https://portswigger.net/web-security/all-labs#api-testing
crAPI (Completely Ridiculous API): https://github.com/OWASP/crAPI
VAmPI (Vulnerable API): https://github.com/erev0s/VAmPI
DVGA (Damn Vulnerable GraphQL App): https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application
Juice Shop (includes APIs): https://github.com/juice-shop/juice-shop
Security Tools
Burp Suite: https://portswigger.net/burp
Postman: https://www.postman.com/
OWASP ZAP: https://www.zaproxy.org/
jwt_tool: https://github.com/ticarpi/jwt_tool
GraphQL Armor: https://github.com/Escape-Technologies/graphql-armor
Arjun (API endpoint discovery): https://github.com/s0md3v/Arjun
Learning Resources
APIsecurity.io: https://apisecurity.io/
GraphQL Security Report 2024: https://escape.tech/blog/the-state-of-graphql-security-2024/
JWT Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
OAuth 2.0 Playground: https://www.oauth.com/playground/
Books
"API Security in Action" by Neil Madden
"Hacking APIs" by Corey Ball
"OAuth 2 in Action" by Justin Richer and Antonio Sanso
Last updated