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=john&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": "john",
"email": "john@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