Python API Development with FastAPI
Advanced FastAPI
Hashing User Password
Earlier, we outlined the process for creating a new user. However, storing passwords as plain text poses a significant security risk. Even if your database is secure now, a breach could expose these passwords to attackers. Instead, always store a hashed version of the password. Hashing is a one-way process that makes it practically impossible to retrieve the original password from its hash.
For instance, running the following SQL command:
select * from users;
reveals that storing plain text passwords (as the query would show) is unsafe. Always hash passwords before saving them to your database.
Tip
FastAPI’s documentation provides an excellent guide on password hashing under the OAuth2 with Password section.
Installing Required Libraries
To implement password hashing, you need two libraries: Passlib (which supports multiple hashing algorithms) and bcrypt (the algorithm we will use). Install them using pip:
pip install passlib[bcrypt]
Alternatively, install both libraries directly:
pip install passlib bcrypt
After installation, verify that both libraries are installed by running pip freeze
.
Application Models and Environment
Below is a snippet from our application models and configurations:
class Post(PostBase):
id: int
created_at: datetime
class Config:
orm_mode = True
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserOut(BaseModel):
id: int
email: EmailStr
created_at: datetime
class Config:
orm_mode = True
And a sample of our environment package list:
colorama==0.4.4
dnspython==2.1.0
email-validator==1.1.3
fastapi==0.68.0
graphene==2.1.9
Configuring the Password Hasher
In your main file, import CryptContext
from Passlib to create a password context that utilizes bcrypt:
from fastapi.params import Body
from pydantic import BaseModel
from passlib.context import CryptContext
from random import randrange
import psycopg2
from psycopg2.extras import RealDictCursor
import time
from sqlalchemy.orm import Session
from sqlalchemy.sql.functions import mode
from . import models, schemas
from .database import engine, get_db
# Configure Passlib to use bcrypt for password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
while True:
# Your database connection logic goes here
break
This configuration sets up the CryptContext to use the bcrypt algorithm for secure password hashing.
Updating the User Registration Endpoint
To ensure passwords are securely stored, update the registration endpoint to hash the password before saving it to the database:
@app.post("/users", status_code=status.HTTP_201_CREATED, response_model=schemas.UserOut)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
# Hash the user's password before storing it in the database
hashed_password = pwd_context.hash(user.password)
user.password = hashed_password
new_user = models.User(**user.dict())
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
A similar version of this endpoint appears as follows:
@app.post("/users/", response_model=schemas.UserOut)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
# Hash the password using Passlib's CryptContext
hashed_password = pwd_context.hash(user.password)
user.password = hashed_password
new_user = models.User(**user.dict())
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
After creating a user, you can confirm that the password has been hashed by running:
select * from users;
Since hashing is a one-way process, retrieving the original password from the hash is not feasible.
Extracting the Hashing Logic for Maintainability
To improve code maintainability, extract the password hashing logic into a separate utility function. Create a new file named utils.py
with the following content:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash(password: str) -> str:
return pwd_context.hash(password)
Then, modify your main.py
to import and use this new utility function:
from sqlalchemy.sql.functions import mode
from . import models, schemas, utils
from .database import engine, get_db
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
while True:
try:
conn = psycopg2.connect(
host='localhost',
database='fastapi',
user='postgres',
password='password123',
cursor_factory=RealDictCursor
)
cursor = conn.cursor()
print("Database connection was successful")
break
except Exception as error:
print("Connecting to database failed")
print("Error:", error)
time.sleep(2)
Update the user registration endpoint to use the utility function for hashing:
@app.post("/users", status_code=status.HTTP_201_CREATED, response_model=schemas.UserOut)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
# Hash the user's password using the utility function from utils.py
hashed_password = utils.hash(user.password)
user.password = hashed_password
new_user = models.User(**user.dict())
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
After testing the endpoint—by creating a new user (e.g., email "[email protected]" with password "password123")—query the users table:
select * from users;
This query will confirm that the application stores only the hashed password, significantly reducing security risks in the event of a data breach.
Summary
By following these steps, you enhance your application's security by ensuring user passwords are hashed rather than stored in plain text. This practice is essential for maintaining user data integrity.
Watch Video
Watch video content