FastAPI has implemented several HTTP security
mechanisms including HTTP sic that can leverage the
Ba
FastAPI’s dependency injection system.
Use the secrets built-in library to compare the provided
username and password with the server’s values. Using
secrets.compare_digest() ensures the duration of
checking operations remain consistent no matter what the
3
inputs are to avoid timing attacks.
Note that secrets.compare_digest() can only accept
byte or string inputs containing ASCII characters (i.e.,
English-only characters). To handle other characters, you
will need to encode the inputs with UTF-8 to bytes first
before performing the credential checks.
Return a standardized authorization HTTPException
compliant with security standards that browsers
understand so that they show the login prompt again to
the user. The exception message must be generic to avoid
leaking any sensitive information, such as the existence of
a user account, to attackers.
Using the HTTPBasic with Depends() returns the
HTTPBasicCredentials object that contains the
provided username.
Injecting a security dependency to any FastAPI endpoint will
protect it with the implemented authentication. You can
experience this yourself now by visiting the /docs page and
sending a request to the /users/me endpoint.
The endpoint will show a lock icon in front of it, and you should
see a sign-in alert when making a request, asking you to
provide credentials, as you can see in Figure 8-2.
Figure 8-2. Basic authentication in FastAPI
Well done! In 25 lines of code, you managed to implement a
basic form of authentication to protect an endpoint. You can
now use basic authentication in your own prototypes and
development servers.
Bear in mind, you should avoid adopting the basic
authentication mechanism in production-grade GenAI services.
A better and more secure alternative for public-facing services
is JWT authentication. It eliminates the need for server-side
sessions by storing all authentication details within a token. It
also maintains data integrity and works across different
domains with a widely accepted standard.
JSON Web Tokens (JWT) Authentication
Now that you’re more familiar with basic concepts of
authentication, let’s implement a more complex but secure JWT
authentication layer to your FastAPI service. As part of this,
you’ll also refactor your existing endpoints to combine them
under a separate resource API router to group, name, tag, and
protect multiple endpoints at once.
What is JWT?
JWTs are a URL-safe and compact way of asserting claims
between applications via tokens.
These tokens consist of three parts:
Headers
Specify the token type and signing algorithm in addition
to the datetime and the issuing authority.
Payload
Specify the body of the token representing the claims on
the resource alongside additional metadata.
Signature
The function that creates the token will also sign it using
the encoded payload, encoded headers, a secret, and the
signing algorithm.
TIP
The base64 encoding algorithm is often used to encode and decode data for
compactness and URL safety.
Figure 8-3 shows what a typical JWT looks like.
Figure 8-3. JWT components (Source: [Link])
JWTs are secure, compact, and convenient since they can hold
all the information needed to perform user authentication,
avoiding the need for multiple database round-trips. In
addition, due to their compactness, you can transfer them
across the network using the HTTP POST body, headers, or URL
parameters.
Getting started with JWT authentication
To get started with implementing the JWT authentication
mechanism in FastAPI, you need to install the passlib and
python-jose dependencies:
$ pip install passlib python-jose
With the dependencies installed, you will then need tables in
the database to store the generated users and associated token
data. For data persistence, let’s migrate the database to create
the users and tokens tables, as shown in Figure 8-4.
Figure 8-4. Entity relationship diagram of users and tokens tables
If you look at Figure 8-4, you will spot that the tokens table
has a one-to-many relationship with the users table. You can
use the token records to track successful login attempts for each
user and to revoke access if needed.
Next, let’s define the required SQLAlchemy models and
Pydantic schemas for database queries and data validation, as
shown in Examples 8-2 and 8-3.
Example 8-2. Declare user SQLAlchemy ORM models
# [Link]
import uuid
from datetime import UTC, datetime
from sqlalchemy import Index, String
from [Link] import DeclarativeBase, Mappe
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[[Link]] = mapped_column(primary
email: Mapped[str] = mapped_column(String(len
hashed_password: Mapped[str] = mapped_column
is_active: Mapped[bool] = mapped_column(defau
role: Mapped[str] = mapped_column(default="US
created_at: Mapped[datetime] = mapped_column
updated_at: Mapped[datetime] = mapped_column
default=[Link](UTC), onupdate=datet
)
__table_args__ = (Index("ix_users_email", "em
You will be using the ORM models at the data access layer while
the Pydantic schemas will validate incoming and outgoing
authentication data at the endpoint layer.
Example 8-3. Declare user Pydantic schemas with username
and password field validators
# [Link]
from datetime import datetime
from typing import Annotated
from pydantic import (UUID4, AfterValidator, Base
validate_call)
@validate_call
def validate_username(value: str) -> str:
if not [Link]():
raise ValueError("Username must be alphan
return value
@validate_call
def validate_password(value: str) -> str:
validations = [
(
lambda v: any([Link]() for cha
"Password must contain at least one d
),
(
lambda v: any([Link]() for cha
"Password must contain at least one u
),
(
lambda v: any([Link]() for cha
"Password must contain at least one l
),
]
for condition, error_message in validations:
if not condition(value):
raise ValueError(error_message)
return value
ValidUsername = Annotated[
str, Field(min_length=3, max_length=20), Afte
]
ValidPassword = Annotated[
str, Field(min_length=8, max_length=64), Afte
]
class UserBase(BaseModel):
model_config = ConfigDict(from_attributes=Tru
username: ValidUsername
is_active: bool = True
role: str = "USER"
class UserCreate(UserBase):
password: ValidPassword
class UserInDB(UserBase):
hashed_password: str
class UserOut(UserBase):
id: UUID4
created_at: datetime
updated_at: datetime
Validate both username and password to enforce higher
security requirements.
Allow Pydantic to read SQLAlchemy ORM model
attributes instead of having to manually populate
Pydantic schemas from SQLAlchemy models.