Signup and Login Endpointsยถ
๐ฏ Goalยถ
Build secure signup and login endpoints using:
- A database-backed
Usermodel - Password hashing with bcrypt
- FastAPIโs OAuth2 password flow
- Form-based login using
x-www-form-urlencoded
This sets the foundation for issuing JWT tokens and protecting routes in the next sublesson.
๐ Why Authentication Mattersยถ
Authentication answers the question:
โWho is this user, and can they prove it?โ
In a secure API:
- Users must register with a unique identity (signup)
- Users must log in with credentials (login)
- Passwords must be stored securely (hashed, never plaintext)
- Authenticated users receive a token to access protected resources
๐ง What Is OAuth2?ยถ
OAuth2 is a widely used protocol for authorization. It defines how clients (like browsers or mobile apps) can obtain access tokens to interact with APIs securely.
FastAPI supports a simplified version called the OAuth2 Password Flow, which is ideal for first-party apps (like your own frontend talking to your own backend).
๐ OAuth2 Password Flowยถ
In this flow:
- The user sends their
usernameandpasswordto the/loginendpoint. - The server verifies the credentials.
- If valid, the server returns an access token (weโll use JWT).
- The client includes this token in future requests to prove identity.
This flow is simple and secure when used over HTTPS and with proper token handling.
๐ง What Is x-www-form-urlencoded?ยถ
This is a format used to send form data in HTTP requests. It looks like this:
username=enrico&password=secret123
Itโs the default format for HTML forms and is required by OAuth2 standards.
FastAPI provides a helper class called OAuth2PasswordRequestForm that automatically parses this format and gives you access to form_data.username and form_data.password.
๐งฑ Step 1: Define the User Modelยถ
๐ models/user.py
from sqlmodel import SQLModel, Field
from typing import Optional
class User(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
username: str = Field(index=True, unique=True)
email: str
hashed_password: str
is_active: bool = True
๐ Why this structure?ยถ
username: used for loginemail: optional for contact or recoveryhashed_password: stores a secure version of the passwordis_active: lets you disable accounts without deleting them
๐ Step 2: Hashing Passwords with bcryptยถ
๐ core/security.py
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
๐ Why hash passwords?ยถ
- Plaintext passwords are dangerous โ if your database leaks, users are exposed.
- Hashing transforms the password into a one-way encrypted string.
- bcrypt adds a salt and is slow by design, making brute-force attacks impractical.
Install the library:
pip install passlib[bcrypt]
๐งฉ Step 3: Signup Endpointยถ
๐ routers/auth.py
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from models.user import User
from db import get_session
from core.security import hash_password
router = APIRouter(prefix="/auth", tags=["Auth"])
@router.post("/signup", status_code=201)
def signup(user: User, session: Session = Depends(get_session)):
existing_user = session.exec(select(User).where(User.username == user.username)).first()
if existing_user:
raise HTTPException(status_code=400, detail="Username already exists")
user.hashed_password = hash_password(user.hashed_password)
session.add(user)
session.commit()
session.refresh(user)
return {"message": "User created", "user_id": user.id}
๐ Whatโs happening?ยถ
- We check if the username already exists.
- We hash the password before storing it.
- We save the user and return a success message.
๐งช Example request:
POST /auth/signup
{
"username": "enrico",
"email": "enrico@example.com",
"hashed_password": "plaintextpassword"
}
โ ๏ธ Note: This uses hashed_password as the field name, but itโs actually receiving a raw password. Weโll fix this later with a custom request model.
๐ Step 4: Login Endpoint with OAuth2ยถ
๐ routers/auth.py (continued)
from fastapi.security import OAuth2PasswordRequestForm
from core.security import verify_password
@router.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session)):
user = session.exec(select(User).where(User.username == form_data.username)).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
return {"message": "Login successful", "user_id": user.id}
๐ Whatโs happening?ยถ
- FastAPI parses the form data using
OAuth2PasswordRequestForm. - We look up the user by username.
- We verify the password using bcrypt.
- If valid, we return a success message (weโll return a token in the next sublesson).
Install the required dependency:
pip install python-multipart
๐งช Try It in Swagger UIยถ
Visit /docs and test:
POST /auth/signupโ Register a new userPOST /auth/loginโ Authenticate with username and password
FastAPI automatically generates a login form for OAuth2.
๐ง Recapยถ
You now have:
| Endpoint | Purpose |
|---|---|
POST /auth/signup |
Register a new user |
POST /auth/login |
Verify credentials |
Youโve implemented:
- Secure password hashing with bcrypt
- Credential verification using OAuth2 password flow
- Form-based login using
x-www-form-urlencoded