- Published on
Build and Secure an API in Python with FastAPI
- Authors
- Name
- Yasser Tahiri
- @THyasser1
Introduction :
As Python grows in popularity, the variety of high-quality frameworks available to developers has blossomed. In addition to steadfast options like Django and Flask, there are many new options including FastAPI.
First released in late 2018, FastAPI differentiates itself from other Python frameworks by offering a modern, fast, and succinct developer experience for building reliable REST APIs. While one of the newer open-source Python frameworks available, FastAPI has quickly gained a following with over 33,000 stars on GitHub and an active community of maintainers working on the project.
Our article is based on Creating an API with high performance built with FastAPI & SQLAlchemy, helps to improve connection with your Backend Side and stay relate using SQLite3 & a secure Schema Based on Python-Jose a JavaScript Object Signing and Encryption implementation in Python.
Getting Started
Initial Setup
Start by creating a new folder to hold your project called "SecureAPI":
$ mkdir SecureAPI
$ cd SecureAPI
- Next, create and activate a virtual environment:
Feel free to swap out virtualenv and Pip for Poetry or Pipenv.
- Create a file
requirements.txt
and add the Preconfigured packages relate to the project
fastapi
uvicorn
sqlalchemy
passlib
bcrypt
python-jose
python-multipart
- I use
pipenv
a global python projectpip install pipenv
. - Create a
virtual environment
for this project
# creating pipenv environment for python 3
$ pipenv --three
# activating the pipenv environment
$ pipenv shell
# if you have multiple python 3 versions installed then
$ pipenv install -d --python 3.8
# install all dependencies (include -d for installing dev dependencies)
$ pipenv install -d
- Next Create the Following Files and Folders :
/---SecureAPI
| .env
| main.py
| requirements.txt
+---api
| | blog.py
| | user.py
+---core
| | auth.py
| | blog.py
| | user.py
+---database
| | configuration.py
+---models
| | models.py
+---schema
| | hash.py
| | oa2.py
| | schemas.py
| | token.py
What we are building?
We are Building an API with high performance built with FastAPI & SQLAlchemy, help to improve connection with your Backend Side to create a simple blog and Cruds with OAuth2PasswordBearer ⛏.
Let's Code :
Database :
- To Provide a good and fast work, i choose
SQLAlchemy
as a toolkit and performance, for the Database i chooseSQLite
a High performance Database and full of Features. Lets Create theConfiguration.py
:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = 'sqlite:///blog.db'
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={
"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
- If you want to configure the Database with an other Provider like
MySQL
orPostgreSQL
you can change theDatabase_URL
here :
# here you need to inster the URI that should be used for the connection.
SQLALCHEMY_DATABASE_URL = 'sqlite:///blog.db'
- For Example to :
SQLALCHEMY_DATABASE_URL = 'mysql://username:password@server/blog'
Models :
Here in the
models.py
, we are gonna create 2 tables based on the requirements related to this projectblogs
andusers
:Lets declare our imports first :
from database.configuration import Base
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
- The Blogs Table :
class Blog(Base):
__tablename__ = "blogs"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
body = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
creator = relationship("User", back_populates="blogs")
- The Users Table :
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
email = Column(String)
password = Column(String)
blogs = relationship("Blog", back_populates="creator")
schemas :
Here we are declaring
Blogs
andUsers
andToken
models, it will contain all formatint
orstring
orbool
, lets code this on ourschemas.py
:declare all
pydantic
&Typing
imports :
from pydantic import BaseModel
from typing import Optional, List
from pydantic.main import BaseConfig
- Our Schema gonna be like this :
class BlogBase(BaseModel):
title: str
body: str
class Blog(BlogBase):
class Config():
orm_mode = True
class User(BaseModel):
name: str
email: str
password: str
class ShowUser(BaseModel):
name: str
email: str
blogs: List[Blog] = []
class Config():
orm_mode = True
class ShowBlog(BaseModel):
title: str
body: str
creator: ShowUser
class Config():
orm_mode = True
class Login(BaseModel):
username: str
password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
API :
Now we need to code the Main API and create the Queries with a hashed Password for the
Users
usingBcrypt
:User.py
:
from sqlalchemy.orm import Session
from schema import schemas
from models import models
from fastapi import HTTPException, status
from schema.hash import Hash
def create(request: schemas.User, db: Session):
hashedPassword = Hash.bcrypt(request.password)
user = models.User(name=request.name, email=request.email,
password=hashedPassword)
db.add(user)
db.commit()
db.refresh(user)
return user
def show(id: int, db: Session):
user = db.query(models.User).filter(models.User.id == id).first()
if not user:
raise HTTPException(status.HTTP_404_NOT_FOUND,
detail=f"User with id {id} not found")
return user
def get_all(db: Session):
return db.query(models.User).all()
- Now lets Create the APi's for the
blog
where we gonna add the Requirements likeCreate
,update
,Show
,Delete
. Blog.py
:
from sqlalchemy.orm import Session
from schema import schemas
from models import models
from fastapi import HTTPException, status
def get_all(db: Session):
blogs = db.query(models.Blog).all()
return blogs
def create(request: schemas.Blog, db: Session):
new_blog = models.Blog(title=request.title, body=request.body, user_id=1)
db.add(new_blog)
db.commit()
db.refresh(new_blog)
return new_blog
def destroy(id: int, db: Session):
blog_to_delete = db.query(models.Blog).filter(models.Blog.id == id)
if not blog_to_delete.first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Blog with id {id} not found.")
blog_to_delete.delete(synchronize_session=False)
db.commit()
return {'done'}
def update(id: int, request: schemas.Blog, db: Session):
blog = db.query(models.Blog).filter(models.Blog.id == id)
if not blog.first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Blog with id {id} not found")
blog.update(request.__dict__)
db.commit()
return 'updated'
def show(id: int, db: Session):
blog = db.query(models.Blog).filter(models.Blog.id == id).first()
if blog:
return blog
else:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Blog with the id {id} is not available")
Core :
- In this folder we gonna Create 3 files
Auth.py
andBlog.py
andUser.py
, all of this files are the routes for our API. - We are gonna Start by
User.py
, where we Create a routes forcreate_user
,get_users
,get_user_by_id
. User.py
:
from fastapi import APIRouter, Depends, Response, status
from sqlalchemy.orm import Session
from schema import schemas
from models import models
from database import configuration
from typing import List
from api import user
router = APIRouter(tags=["Users"], prefix="/users")
get_db = configuration.get_db
# Create User
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.ShowUser)
def create_user(request: schemas.User, db: Session = Depends(get_db)):
return user.create(request, db)
# Get Users
@router.get("/", status_code=status.HTTP_200_OK, response_model=List[schemas.ShowUser])
def get_users(db: Session = Depends(get_db)):
return user.get_all(db)
# Get Users using the ID
@router.get("/{id}", status_code=status.HTTP_200_OK, response_model=schemas.ShowUser)
def get_user_by_id(id: int, db: Session = Depends(get_db)):
return user.show(id, db)
- Now we need to add the
Blog.py
, relate to the API where we provide all Requirements. Blog.py
:
from schema.oa2 import get_current_user
from fastapi import APIRouter, Depends, status, Response
from schema import schemas
from database import configuration
from typing import List
from sqlalchemy.orm import Session
from api import blog
router = APIRouter(tags=["Blogs"], prefix="/blog")
get_db = configuration.get_db
@router.get("/", response_model=List[schemas.ShowBlog])
def get_all_blogs(db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)):
return blog.get_all(db)
@router.post("/", status_code=status.HTTP_201_CREATED)
def create(request: schemas.Blog, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)):
return blog.create(request, db)
@router.get("/{id}", status_code=status.HTTP_200_OK, response_model=schemas.ShowBlog)
def get_blog_by_id(id: int, response: Response, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)):
return blog.show(id, db)
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_blog(id: int, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)):
return blog.destroy(id, db)
@router.put("/{id}", status_code=status.HTTP_202_ACCEPTED)
def update_blog(id: int, request: schemas.Blog, db: Session = Depends(get_db), current_user: schemas.User = Depends(get_current_user)):
return blog.update(id, request, db)
- Here we mention some Route for Authentication
login
, that why we create theAuth.py
to create a route relate to login for the user. Auth.py
:
from fastapi.security.oauth2 import OAuth2PasswordRequestForm
from schema.token import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from schema import schemas
from models import models
from database import configuration
from sqlalchemy.orm import Session
from schema.hash import Hash
router = APIRouter(prefix="/login",tags=["Authentication"])
@router.post("/")
def login(request: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(configuration.get_db)):
user: schemas.User = db.query(models.User).filter(
models.User.email == request.username).first()
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Invalid Credentials")
if not Hash.verify(user.password, request.password):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Incorrect password")
# access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email}
)
# generate JWT token and return
return {"access_token": access_token, "token_type": "bearer"}
about
from schema.hash import Hash
we gonna describe it on the Security Side.
- Now we Create all our routes we need to Secure them that why i use
fastapi.security
to implement and useOAuth2PasswordBearer
for Authentication.
Security :
- Back to the
schema
folder we need to describe and code some files relate to JWT "Json Web Token" using The bcrypt also to hash all password relate to login and Authentication. hash.py
:
from passlib.context import CryptContext
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
class Hash():
@staticmethod
def bcrypt(password: str):
return pwd_ctx.hash(password)
def verify(hashed_password, plain_password):
return pwd_ctx.verify(plain_password, hashed_password)
- Now, we need to code our
token.py
after define it on the schemas file as aTokendata
, usingPython-jose
tocreate_access_token
andverify_token
also for generate aSecret_Key
and put it into the.env
file. Token.py
:
from typing import Optional
from datetime import timedelta, datetime
from jose import jwt, JWTError
from schema.schemas import TokenData
import os
basedir = os.path.abspath(os.path.dirname(__file__))
SECRET_KEY = str(os.environ.get("SECRET_KEY"))
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = str(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES"))
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 verify_token(token: str, credentials_exception):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=ALGORITHM)
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
except JWTError:
raise credentials_exception
To generate a
SECRET_KEY
u can useopenssl rand -hex 32
.
- Now we need to write our
SECRET_KEY
andACCESS_TOKEN_EXPIRE_MINUTES
into the enviromment variables file : .env
:
SECRET_KEY = <Generate your Secret_key>
ACCESS_TOKEN_EXPIRE_MINUTES = <estimate Time "int">
- We didn't finish yet, we need now to write a
oa2.py
is the OAuth 2.0 an industry-standard protocol for authorization. - This file is relate to our
Token
cause when weget_current_user
we need to verify thetoken
after login when we useoauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
. oa2.py
:
from fastapi import HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer
from schema.token import verify_token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "bearer"},)
return verify_token(token, credentials_exception=credentials_exception)
- Now after creating all Requirements files and folders we need to run the project, that why we need to code the
main.py
where we gonnaCreate the database tables
andCreate the instance
alsoincludes all Routes
. - Here we gonna import all the basics files to run the Project :
from fastapi import FastAPI
from starlette.responses import HTMLResponse
# import locale files
from models import models
from database.configuration import engine
# import router files
from core import blog, user, auth
- Now lets Create the database tables & the instance also includes all Routes :
models.Base.metadata.create_all(bind=engine)
app = FastAPI(
title="SecureAPI",
description="Hello thank you for reading my Blog UwU",
version="1.0.0",)
app.include_router(blog.router)
app.include_router(user.router)
app.include_router(auth.router)
- By default FastAPI return the response using
JSONResponse
, but we will Custom our Response usingstarlette.responses
andHTMLResponse
:
@app.get("/", response_class=HTMLResponse)
def index():
return """
<!Doctype html>
<html>
<body>
<h1>SecureAPI</h1>
<div class="btn-group">
<a href="/docs"><button>SwaggerUI</button></a>
<a href="/redoc"><button>Redoc</button></a>
</div>
</body>
</html>
"""
- With this we can return a simple
index
with 2 buttons can redirect us to visualize and interact with the API’s resources without having any of the implementation logic in place. - Read more Here about SWAGGER UI or Redoc.
Running the Application
- To run the
Main.py
we need to use uvicorn a lightning-fast ASGI server implementation, using uvloop and httptools.
# Running the application using uvicorn
$ uvicorn main:app --reload
- Voila! Now the API should be Secure and work in a high level ❤
- Try Now to play and interact with the enviromment to get more knowledges about APIs and also FastAPI.
Conclusion
- This Article covered the process of Buidling and Securing a FastAPI application with JSON Web Tokens. You can find the Simular Source Code under the Name of Doge API Yezz123/DogeAPI. Thanks for reading.