Every API endpoint is a potential attack surface. Unauthenticated endpoints leak data. Weak authentication enables account takeover. Missing authorization allows users to access others’ data. API security mistakes lead to breaches, regulatory problems, and lost trust.
This guide covers authentication (proving identity) and authorization (controlling access) patterns for modern APIs.
Authentication Fundamentals
Authentication verifies that a request comes from who it claims to come from.
API Keys
Simple authentication for server-to-server communication:
GET /api/data HTTP/1.1
Authorization: Bearer api_key_abc123
Advantages:
- Simple to implement and use
- No session management
- Easy to rotate
Disadvantages:
- No user identity (key identifies application, not user)
- Shared secrets are risky
- No fine-grained permissions (usually all or nothing)
Best practices:
- Transmit in headers, not URLs (URLs get logged)
- Use different keys for different environments
- Rotate keys periodically
- Monitor key usage for anomalies
- Implement key scoping when possible
API keys suit internal services and server-to-server communication. They’re inappropriate for client applications where keys can’t be kept secret.
JWT (JSON Web Tokens)
Signed tokens that contain claims about the bearer:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0IiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWTs have three parts:
- Header: Algorithm and token type
- Payload: Claims (user ID, expiration, permissions)
- Signature: Verification that the token hasn’t been tampered with
Advantages:
- Self-contained (no server-side session storage)
- Can include custom claims
- Verifiable without database lookup
Disadvantages:
- Can’t be revoked once issued
- Size (tokens can be large)
- Security depends on proper implementation
Best practices:
- Use appropriate algorithms (RS256 for asymmetric, HS256 for symmetric)
- Set reasonable expiration times (hours, not days)
- Include only necessary claims (minimize size and exposure)
- Validate all claims on every request
- Use refresh tokens for extending sessions
# Verification example
import jwt
def verify_token(token):
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
options={"require": ["exp", "sub"]}
)
return payload
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
OAuth 2.0
Standard protocol for delegated authorization. Users authorize applications to act on their behalf without sharing credentials.
Grant types:
- Authorization Code: For server-side apps. Most secure.
- PKCE (Proof Key for Code Exchange): For mobile/SPA apps. Secure without client secrets.
- Client Credentials: For server-to-server. Application authenticates itself.
- Implicit (deprecated): Avoided due to security weaknesses.
When to use:
- When users authorize third-party access to their data
- When you’re building an API platform
- When integrating with other services that support OAuth
OAuth is complex but standardized. Use established libraries rather than implementing from scratch.
Multi-Factor Authentication
For sensitive operations, require additional verification:
- Something you know (password)
- Something you have (phone, hardware key)
- Something you are (biometrics)
MFA significantly reduces account takeover risk. Implement for:
- Administrative operations
- Password changes
- High-value transactions
- First login from new device
Authorization Patterns
Authentication proves identity; authorization determines what that identity can access.
Role-Based Access Control (RBAC)
Users are assigned roles; roles have permissions:
User -> Role -> Permissions
Alice -> admin -> [read, write, delete, admin]
Bob -> user -> [read, write]
Carol -> viewer -> [read]
Implementation:
ROLE_PERMISSIONS = {
"admin": ["read", "write", "delete", "admin"],
"user": ["read", "write"],
"viewer": ["read"]
}
def has_permission(user, permission):
role_perms = ROLE_PERMISSIONS.get(user.role, [])
return permission in role_perms
# Usage
if has_permission(current_user, "write"):
# Allow write operation
RBAC is simple and works well when roles map cleanly to permissions. It struggles with fine-grained or context-dependent access.
Attribute-Based Access Control (ABAC)
Access decisions based on attributes of the user, resource, and context:
def can_access(user, resource, action):
# User must be authenticated
if not user.authenticated:
return False
# Admins can do anything
if user.role == "admin":
return True
# Users can read their own resources
if action == "read" and resource.owner_id == user.id:
return True
# Users can read public resources
if action == "read" and resource.visibility == "public":
return True
# Managers can read their team's resources
if action == "read" and resource.team_id in user.managed_teams:
return True
return False
ABAC is more flexible than RBAC but more complex to implement and reason about.
Resource-Based Authorization
Authorization at the resource level:
@app.route("/documents/<doc_id>", methods=["GET"])
def get_document(doc_id):
user = get_current_user()
document = Document.query.get(doc_id)
if not document:
return {"error": "Not found"}, 404
if not can_access(user, document, "read"):
return {"error": "Forbidden"}, 403
return document.to_dict()
Always check authorization after retrieving the resource. Returning 404 for unauthorized access (rather than 403) can prevent resource enumeration.
OAuth Scopes
OAuth tokens can include scopes limiting what the token can do:
scope: "read:documents write:documents"
Applications request scopes; users approve them; APIs enforce them:
def require_scope(required_scope):
def decorator(f):
def wrapper(*args, **kwargs):
token_scopes = get_token_scopes()
if required_scope not in token_scopes:
return {"error": "Insufficient scope"}, 403
return f(*args, **kwargs)
return wrapper
return decorator
@app.route("/documents", methods=["POST"])
@require_scope("write:documents")
def create_document():
# ...
Scopes enable users to grant limited access. A read-only integration doesn’t need write permission.
Common Vulnerabilities
Broken Authentication
- Weak passwords allowed
- No rate limiting on login (allows brute force)
- Predictable password reset tokens
- Credential stuffing not detected
Mitigations:
- Enforce password complexity
- Rate limit authentication attempts
- Use secure random tokens
- Detect and block credential stuffing
Broken Authorization
- Horizontal privilege escalation (accessing other users’ data)
- Vertical privilege escalation (gaining admin privileges)
- Missing authorization checks on endpoints
Mitigations:
- Check authorization on every request
- Use consistent authorization patterns
- Test authorization with different user types
- Log and monitor access patterns
Token Security Issues
- Tokens stored insecurely (localStorage for sensitive apps)
- Tokens in URLs (logged in server logs)
- Long-lived tokens with no revocation
- Algorithm confusion attacks on JWTs
Mitigations:
- Store tokens securely (httpOnly cookies for web)
- Transmit tokens in headers
- Use short-lived tokens with refresh
- Explicitly specify expected algorithms
Implementation Checklist
Authentication
- Use HTTPS for all API traffic
- Implement rate limiting on authentication endpoints
- Hash passwords with modern algorithms (bcrypt, argon2)
- Use secure, random session/token generation
- Implement token expiration
- Provide token revocation capability
- Log authentication events
Authorization
- Check authorization on every request
- Use deny-by-default (require explicit permission)
- Implement resource-level authorization
- Validate user input (prevent injection)
- Log authorization failures
- Test with different user roles and permissions
API Security
- Return consistent error responses (don’t leak information)
- Implement request signing for high-security operations
- Use idempotency keys for non-idempotent operations
- Validate Content-Type headers
- Set appropriate security headers
Key Takeaways
- Choose authentication method based on use case: API keys for server-to-server, JWT for stateless auth, OAuth for delegated access
- Always validate tokens completely: signature, expiration, issuer, audience
- Implement authorization checks on every request; deny by default
- Use scopes to limit token capabilities to minimum necessary
- Log authentication and authorization events for security monitoring
- Test security with different user roles and attack scenarios