Securing APIs: Authentication and Authorization Patterns

December 19, 2016

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:

Disadvantages:

Best practices:

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:

  1. Header: Algorithm and token type
  2. Payload: Claims (user ID, expiration, permissions)
  3. Signature: Verification that the token hasn’t been tampered with

Advantages:

Disadvantages:

Best practices:

# 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:

When to use:

OAuth is complex but standardized. Use established libraries rather than implementing from scratch.

Multi-Factor Authentication

For sensitive operations, require additional verification:

MFA significantly reduces account takeover risk. Implement for:

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

Mitigations:

Broken Authorization

Mitigations:

Token Security Issues

Mitigations:

Implementation Checklist

Authentication

Authorization

API Security

Key Takeaways