FastAPI TODO: Adding User Authentication With Username/Password
Securing your FastAPI application with authentication is crucial, especially when dealing with user-specific data like a TODO list. This article walks you through implementing user-based access control in your FastAPI TODO application using username and password authentication, while also keeping command-line interface (CLI) access in mind. We'll cover the essential steps, from setting up password hashing to creating API endpoints and handling CLI authentication.
Understanding Authentication in FastAPI
When dealing with authentication in FastAPI, it's essential to grasp the core concepts. Authentication verifies a user's identity, confirming they are who they claim to be. This is crucial for securing your API and ensuring that only authorized users can access specific resources. In our case, we want to ensure that users can only access and modify their own TODO items. Implementing a robust authentication system involves several key steps, including creating user models, hashing passwords, generating and verifying tokens, and setting up authentication dependencies within your FastAPI application. Choosing the right authentication method depends on your application's specific needs and security requirements.
User-based authentication adds a layer of security by requiring users to log in with unique credentials before accessing their data. In this context, we'll use a username and password combination. This method is widely used and relatively straightforward to implement. However, security is paramount. We must never store passwords in plain text. Instead, we'll use password hashing techniques to securely store user credentials. Furthermore, we'll explore how to manage user sessions and tokens to maintain authentication across multiple requests. This approach ensures that each user's TODO list remains private and protected, enhancing the overall security and integrity of your application.
Furthermore, the integration with a CLI presents a unique challenge. While web-based applications often rely on cookies or local storage for session management, CLIs require a different approach. We'll explore methods such as API keys or token-based authentication that can be easily used in a CLI environment. This will involve designing our authentication system to be flexible enough to accommodate both web and CLI access. This flexibility is crucial for applications that need to be accessible through multiple interfaces, ensuring a consistent and secure user experience across all platforms. By carefully considering the needs of both web and CLI users, we can create a robust authentication system that meets all our requirements.
Setting Up Your FastAPI Project
Before diving into the authentication implementation, ensure you have a FastAPI project set up. If you're starting from scratch, you'll need to install FastAPI and Uvicorn (an ASGI server) using pip:
pip install fastapi uvicorn
You'll also likely need an async database driver like databases and a database such as PostgreSQL or SQLite. For password hashing, we'll use passlib and bcrypt. Install these as well:
pip install passlib bcrypt python-jose python-multipart
With the necessary packages installed, you can begin structuring your project. A typical FastAPI project structure might include directories for models, routes, and database connections. Organizing your project in this way promotes maintainability and scalability. For instance, you might have a models.py file to define your database models, a routes/ directory to house your API endpoints, and a database.py file to manage database connections. This modular approach makes it easier to navigate and modify your codebase as your application grows. Furthermore, it helps to separate concerns, making it clearer where each piece of functionality resides. This well-structured foundation is essential for implementing authentication effectively and ensuring your application is secure and maintainable.
Remember that setting up your project correctly from the outset can save you significant time and effort in the long run. A clear and organized structure makes it easier to add new features, debug issues, and collaborate with other developers. Taking the time to establish this foundation will pay dividends as your FastAPI application evolves. Consider using version control systems like Git to track changes and collaborate effectively. By incorporating these best practices, you'll be well-prepared to tackle the complexities of user authentication and other advanced features.
Creating User Models and Database Setup
First, define a user model using SQLAlchemy or another ORM. This model will represent users in your database and include fields like id, username, and a hashed password.
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./todo.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
Base.metadata.create_all(bind=engine)
This user model is the foundation of your authentication system. It defines the structure of the user data that will be stored in your database. The id field serves as the primary key, uniquely identifying each user. The username field stores the user's chosen username, and the hashed_password field stores the user's password after it has been securely hashed. Storing hashed passwords instead of plain text is crucial for security. If your database were compromised, attackers would not be able to directly access user passwords. The unique=True constraint on the username field ensures that each user has a distinct username, preventing potential conflicts. By defining this model, you're setting the stage for storing and retrieving user information securely and efficiently.
Next, implement functions to interact with the database. This includes creating a database session and functions to create users, retrieve users by username, and verify passwords.
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_user(db: SessionLocal, username: str):
return db.query(User).filter(User.username == username).first()
def create_user(db: SessionLocal, username: str, password: str):
hashed_password = get_password_hash(password)
db_user = User(username=username, hashed_password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
These database interaction functions are essential for managing user accounts. The get_password_hash function uses passlib to securely hash passwords before storing them in the database. This one-way function transforms the password into an irreversible hash, protecting it from unauthorized access. The verify_password function compares a plain text password with a hashed password using passlib's verify method. This ensures that the entered password matches the stored hash without revealing the actual password. The get_user function retrieves a user from the database by their username, while the create_user function creates a new user, hashing the password before saving it. By implementing these functions, you're building a secure and efficient system for managing user credentials within your FastAPI application.
Implementing Authentication Endpoints
Now, let's create the API endpoints for user registration and login. These endpoints will handle user creation and authentication, generating tokens for authenticated users.
from fastapi import Depends, HTTPException, status, APIRouter
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from . import models, database
from . import auth
router = APIRouter()
@router.post("/register", response_model=schemas.User)
async def register_user(user: schemas.UserCreate, db: Session = Depends(database.get_db)):
db_user = database.get_user(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
return database.create_user(db=db, username=user.username, password=user.password)
@router.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(database.get_db)):
user = auth.authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"},)
access_token = auth.create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
These API endpoints are the gateway to your authentication system. The /register endpoint allows new users to create accounts. It first checks if the username is already taken, and if not, it creates a new user in the database using the create_user function. The /token endpoint handles user login. It uses OAuth2PasswordRequestForm to receive the username and password, then calls auth.authenticate_user to verify the credentials. If the user is authenticated, it generates an access token using auth.create_access_token and returns it to the client. This token will be used to authenticate subsequent requests. By implementing these endpoints, you're providing the necessary functionality for users to register and log in to your application.
Generating and Using JWT Tokens
JSON Web Tokens (JWT) are a standard for securely transmitting information between parties as a JSON object. We'll use JWTs to manage user sessions after successful login.
from datetime import timedelta, datetime
from typing import Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from . import schemas, database
SECRET_KEY = "YOUR_SECRET_KEY" # Change this in production!
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(data: dict, expires_delta: Optional[timedelta] = 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
def authenticate_user(db: Session, username, password):
user = database.get_user(db, username=username)
if not user:
return None
if not database.verify_password(password, user.hashed_password):
return None
return user
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)):
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
token_data = schemas.TokenData(username=username)
except JWTError:
raise credentials_exception
user = database.get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user
JWTs are the backbone of secure session management in modern web applications. The create_access_token function generates a JWT that contains user information and an expiration time. This token is then sent to the client upon successful login. The authenticate_user function verifies the user's credentials against the database. The get_current_user function is a dependency that you'll use in your API endpoints to authenticate users. It extracts the JWT from the request header, decodes it, and retrieves the user from the database. If the token is invalid or the user cannot be found, it raises an exception. By using JWTs, you can securely manage user sessions and protect your API endpoints from unauthorized access.
Remember to replace "YOUR_SECRET_KEY" with a strong, randomly generated secret key in a production environment. This key is used to sign the JWTs, and a weak key could compromise the security of your application. Consider storing the secret key in an environment variable to avoid hardcoding it in your application.
Securing API Endpoints
With the authentication system in place, you can now secure your API endpoints by adding a dependency on get_current_user. This ensures that only authenticated users can access these endpoints.
from fastapi import APIRouter, Depends
from . import schemas
from .auth import get_current_user
from .database import SessionLocal
from sqlalchemy.orm import Session
router = APIRouter()
@router.get("/todos", response_model=list[schemas.Todo])
async def read_todos(current_user: schemas.User = Depends(get_current_user), db: Session = Depends(database.get_db)):
# Fetch todos for the current user
return db.query(models.Todo).filter(models.Todo.owner_id == current_user.id).all()
@router.post("/todos", response_model=schemas.Todo)
async def create_todo(todo: schemas.TodoCreate, current_user: schemas.User = Depends(get_current_user), db: Session = Depends(database.get_db)):
# Create a new todo for the current user
db_todo = models.Todo(**todo.dict(), owner_id=current_user.id)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
By adding current_user: schemas.User = Depends(get_current_user) as a dependency to your API endpoints, you're enforcing authentication. FastAPI will automatically call the get_current_user function before executing the endpoint logic. If get_current_user raises an exception (e.g., due to an invalid token), FastAPI will return an HTTP 401 Unauthorized error. If authentication is successful, the current_user parameter will contain the authenticated user's information. You can then use this information to personalize the response or restrict access to specific resources. For example, in the /todos endpoint, we're filtering the TODO items to only return those that belong to the current user. This ensures that users can only access their own data, enhancing the security and privacy of your application.
Handling CLI Authentication
For CLI access, you can use a similar token-based authentication mechanism. Instead of relying on cookies or sessions, the CLI can store the access token and include it in the headers of API requests.
When designing CLI authentication, it's crucial to consider how the CLI will obtain and store the access token. One common approach is to have the CLI prompt the user for their username and password, then make a request to the /token endpoint to obtain an access token. The CLI can then store this token securely, such as in a configuration file or a secure storage mechanism provided by the operating system. When making subsequent API requests, the CLI should include the access token in the Authorization header, typically using the Bearer scheme (e.g., Authorization: Bearer <access_token>).
To facilitate this, you might need to provide a CLI command or option that allows users to log in and store their access token. You might also want to implement a command to clear the stored token, effectively logging the user out. Ensure that the CLI stores the token securely, using encryption or other appropriate measures to protect it from unauthorized access. By implementing these mechanisms, you can provide a secure and convenient way for users to interact with your FastAPI application from the command line.
Conclusion
Adding user-based authentication to your FastAPI application is essential for securing your data and ensuring that only authorized users have access. By following the steps outlined in this article, you can implement a robust authentication system that supports both web and CLI access. Remember to prioritize security best practices, such as using strong passwords, hashing passwords securely, and protecting your secret key.
For more information on FastAPI security and authentication, you can refer to the official FastAPI documentation and resources like the FastAPI Security Tutorial.