Skip to content

Signup and Login Endpointsยถ

๐ŸŽฏ Goalยถ

Build secure signup and login endpoints using:

  • A database-backed User model
  • 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:

  1. The user sends their username and password to the /login endpoint.
  2. The server verifies the credentials.
  3. If valid, the server returns an access token (weโ€™ll use JWT).
  4. 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 login
  • email: optional for contact or recovery
  • hashed_password: stores a secure version of the password
  • is_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 user
  • POST /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