FastAPI Authentication & Authorization: A Complete Guide

by Jhon Lennon 57 views

Hey guys! Ever wondered how to secure your FastAPI applications like a pro? Well, you've come to the right place! This guide will walk you through the ins and outs of authentication and authorization in FastAPI, making sure your APIs are safe and sound. We'll cover everything from basic concepts to practical implementation, so buckle up and let's dive in!

What are Authentication and Authorization?

Before we jump into the code, let's clarify what authentication and authorization actually mean. These two terms are often used together, but they serve different purposes.

Authentication is all about verifying who a user is. Think of it like showing your ID to get into a club. You're proving that you are who you say you are. In the context of APIs, this usually involves checking a username and password, an API key, or a token.

Authorization, on the other hand, is about determining what a user is allowed to do. Once you're inside the club (authenticated), the bouncer might only allow you into certain areas based on your VIP status. Similarly, in APIs, authorization checks whether an authenticated user has the necessary permissions to access specific resources or perform certain actions.

In simpler terms:

  • Authentication: "Are you who you claim to be?"
  • Authorization: "Are you allowed to do this?"

Why are these important? Well, without authentication, anyone could pretend to be someone else and access sensitive data. Without authorization, even authenticated users could do things they shouldn't, like deleting other users' accounts or accessing confidential information. So, securing your APIs with both authentication and authorization is crucial for protecting your data and your users.

Think about building a social media app. Authentication ensures that only the real John Doe can log in as John Doe. Authorization makes sure that John can only see his posts and not everyone else's private messages. He can post, edit his own posts, and delete his own posts. But he cannot delete other users' accounts, or access the admin panel, unless he has the correct roles.

In short, authentication confirms identity, and authorization manages permissions. Together, they form a robust security system for your FastAPI applications. It's like having a lock on your door (authentication) and a set of rules about who can enter which rooms (authorization). Skipping either step is like leaving your house unlocked or giving everyone a master key – a recipe for disaster!

Authentication Methods in FastAPI

Okay, so now that we know why we need authentication, let's talk about how to implement it in FastAPI. There are several methods you can use, each with its own pros and cons. Here are some of the most common ones:

1. HTTP Basic Authentication

This is the simplest form of authentication. The client sends the username and password in the Authorization header, encoded in Base64. It's easy to implement, but it's not very secure because the credentials are sent in plain text (though encoded, it is easily decoded) unless you're using HTTPS.

When to use it: For quick prototypes or internal APIs where security isn't a major concern.

Example:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):  
    if credentials.username == "test" and credentials.password == "test":
        return credentials.username
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Incorrect email or password",
        headers={"WWW-Authenticate": "Basic"},
    )


@app.get("/users/me")
async def read_current_user(username: str = Depends(get_current_user)):  
    return {"username": username}

2. API Key Authentication

With API key authentication, the client sends a unique key in the header, query parameter, or cookie. The server then validates this key against a stored list of valid keys. This is more secure than Basic Authentication, but it's still vulnerable to key theft if the key is not properly protected.

When to use it: For APIs that are consumed by third-party applications or services.

Example:

from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import APIKeyHeader

app = FastAPI()

API_KEY = "YOUR_API_KEY"

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)


async def get_api_key(api_key_header: str = Depends(api_key_header)):
    if api_key_header == API_KEY:
        return api_key_header
    else:
        raise HTTPException(status_code=400, detail="Invalid API Key")


@app.get("/items/")
async def read_items(api_key: str = Depends(get_api_key)):  
    return {"message": "Items retrieved successfully"}

3. OAuth 2.0 and JWT (JSON Web Tokens)

OAuth 2.0 is a powerful authorization framework that allows users to grant limited access to their resources on one site to another site, without sharing their credentials. JWTs are often used with OAuth 2.0 to securely transmit information between parties as a JSON object.

When to use it: For complex applications that require secure and scalable authentication and authorization.

Example:

This is a more involved process, but here's a simplified example using fastapi.security and passlib for password hashing:

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Union
import jwt


app = FastAPI()

SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


# Dummy user database (replace with a real database in production)
users = {
    "testuser": {
        "username": "testuser",
        "hashed_password": pwd_context.hash("testpassword")
    }
}


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception

    except jwt.PyJWTError:
        raise credentials_exception
    user = users.get(username)
    if user is None:
        raise credentials_exception
    return user


async def get_current_active_user(current_user: dict = Depends(get_current_user)):
    return current_user


@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = users.get(form_data.username)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    if not verify_password(form_data.password, user["hashed_password"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user["username"]}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me")
async def read_users_me(current_user: dict = Depends(get_current_active_user)):
    return {"username": current_user["username"]}

This example demonstrates:

  • Password Hashing: Using passlib to securely hash and verify passwords.
  • JWT Creation: Creating JWTs upon successful login.
  • Dependency Injection: Using FastAPI's dependency injection to validate the JWT and retrieve the current user.

Authorization Techniques in FastAPI

Once you've authenticated your users, you need to control what they can access. Here's how to handle authorization in FastAPI:

1. Role-Based Access Control (RBAC)

RBAC is a common approach where you assign roles to users and then grant permissions to those roles. For example, you might have roles like "admin", "editor", and "viewer". The "admin" role might have permission to create, read, update, and delete resources, while the "viewer" role might only have permission to read them.

How to implement it:

  • Store user roles in your database.
  • Create dependencies that check if the current user has the required role before accessing a route.

Example:

from fastapi import FastAPI, Depends, HTTPException, status
from typing import List

app = FastAPI()

# Dummy user data with roles
users = {
    "john": {"username": "john", "roles": ["editor"]},
    "jane": {"username": "jane", "roles": ["viewer"]},
    "admin": {"username": "admin", "roles": ["admin"]},
}


async def get_current_user(username: str):
    user = users.get(username)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user


async def has_role(required_roles: List[str], current_user: dict = Depends(get_current_user)):
    user_roles = current_user["roles"]
    if any(role in user_roles for role in required_roles):
        return True
    else:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient privileges"
        )


@app.get("/articles/")
async def read_articles(user_has_editor_role: bool = Depends(lambda: has_role(["editor", "admin"], Depends(lambda: get_current_user("john"))))):
    return {"message": "Articles for editors and admins"}


@app.get("/dashboard/")
async def read_dashboard(user_has_admin_role: bool = Depends(lambda: has_role(["admin"], Depends(lambda: get_current_user("admin"))))):
    return {"message": "Dashboard for admins only"}

2. Attribute-Based Access Control (ABAC)

ABAC is a more flexible approach where you define access control policies based on attributes of the user, the resource, and the environment. For example, you might allow a user to access a document only if they are the owner of the document, the document is not classified as confidential, and the current time is during business hours.

How to implement it:

  • Define a policy engine that can evaluate access control policies.
  • Implement dependencies that use the policy engine to check if the current user has access to the resource.

Example:

Implementing a full ABAC system is complex and often involves dedicated libraries or services. Here's a simplified illustration:

from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()

# Dummy data
documents = {
    "doc1": {"owner": "john", "classification": "public"},
    "doc2": {"owner": "jane", "classification": "confidential"},
}


async def get_current_user():
    # In a real application, this would fetch the user from a database
    return {"username": "john"}


async def check_document_access(document_id: str, current_user: dict = Depends(get_current_user)):
    document = documents.get(document_id)
    if not document:
        raise HTTPException(status_code=404, detail="Document not found")

    # Access control policy:
    # - Owner can access any document
    # - Others can only access public documents
    if document["owner"] == current_user["username"]:
        return True  # Owner has access
    elif document["classification"] == "public":
        return True  # Public document
    else:
        raise HTTPException(status_code=403, detail="Access denied")


@app.get("/documents/{document_id}")
async def read_document(document_id: str, has_access: bool = Depends(check_document_access)):
    return {"message": f"Document {document_id} accessed successfully"}

3. Implementing Custom Logic

Sometimes, you need more fine-grained control over access than RBAC or ABAC can provide. In these cases, you can implement custom authorization logic directly in your route handlers or dependencies.

How to implement it:

  • Write code that checks specific conditions based on your application's requirements.
  • Raise an HTTPException with a 403 Forbidden status code if the user is not authorized.

Example:

from fastapi import FastAPI, Depends, HTTPException

app = FastAPI()

# Dummy data
items = {
    1: {"owner": "john", "data": "John's item"},
    2: {"owner": "jane", "data": "Jane's item"},
}


async def get_current_user():
    # In a real application, this would fetch the user from a database
    return {"username": "john"}


async def check_item_ownership(item_id: int, current_user: dict = Depends(get_current_user)):
    item = items.get(item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")

    if item["owner"] != current_user["username"]:
        raise HTTPException(status_code=403, detail="Not the item owner")
    return item


@app.get("/items/{item_id}")
async def read_item(item: dict = Depends(check_item_ownership)):
    return {"data": item["data"]}

Best Practices for Authentication and Authorization

Alright, now that we've covered the basics, let's talk about some best practices to keep in mind when implementing authentication and authorization in your FastAPI applications.

  1. Use HTTPS: Always use HTTPS to encrypt communication between the client and the server. This prevents attackers from eavesdropping on the connection and stealing credentials.
  2. Hash Passwords: Never store passwords in plain text. Always hash them using a strong hashing algorithm like bcrypt or Argon2.
  3. Use Secure Tokens: When using JWTs, make sure to use a strong secret key and a secure signing algorithm like HS256 or RS256. Store the secret key securely and rotate it regularly.
  4. Validate All Input: Always validate all input from the client to prevent injection attacks. This includes validating usernames, passwords, API keys, and tokens.
  5. Implement Rate Limiting: Implement rate limiting to prevent brute-force attacks. This limits the number of requests that a client can make in a given time period.
  6. Regularly Review and Update Your Security Measures: Security is an ongoing process. Regularly review and update your authentication and authorization mechanisms to address new threats and vulnerabilities.
  7. Principle of Least Privilege: Grant users only the minimum level of access they need to perform their tasks. Avoid giving users more permissions than necessary.
  8. Centralized Authentication and Authorization: For larger applications, consider using a centralized authentication and authorization server (like Keycloak or Auth0) to manage users and permissions.

Conclusion

So, there you have it! A comprehensive guide to authentication and authorization in FastAPI. We've covered the fundamental concepts, different authentication methods, authorization techniques, and best practices. By following these guidelines, you can build secure and robust APIs that protect your data and your users.

Remember, security is not a one-time task but an ongoing process. Stay informed about the latest security threats and vulnerabilities, and continuously improve your security measures to keep your FastAPI applications safe and sound. Now go forth and build secure APIs, my friends!