My version of authentication using JWT in FastAPI + React
When creating their pet projects, many people have the question of user authentication. This may be due to personal display of pages, access settings, etc.
In this article I want to show my solution to the issue. I’ll say right away that it may not be ideal, there may be other solutions, but it seems to me that for a pet project, and maybe more, this is quite enough.
What will we use?
I decided to experiment with the JWT (JSON web token) token, since it does not need to be stored in the database or somewhere on the server, this simplifies the application architecture.
To work we need to install PyJWT
pip install pyjwt
In addition, you can install passlib for password hashing
pip install "passlib [bcrypt]"
How does this work?
Our token is a long string without spaces, it looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
It is not encrypted, so anyone can get information from the token, you can look at this at https://jwt.io/. But, although the token is not encrypted, it is signed. Therefore, when we receive the token we sent, we can check it to make sure it was really us who sent it.
Also, the token has an expiration date, so we can create it for a month, a week, an hour, etc. If some dishonest person tries to change the expiration date or encoded information, we will detect this when decoding the token.
The token itself is stored on the client side, so we don't need to allocate space to store it on the server.
Our system should work something like this:
When a user registers or logs into an account, we issue a new token, into which we encode the creation date and the necessary data -> the client receives this token and stores it on its side -> the next time the client accesses the server, the client sends the token -> the server decrypts the token and returns the result.
Implementing the server side
For this I created python file and class UserAuth. In this class we implement the methods:
Creating a token
data – data to be encoded into a token, expires_delta – token expiration time
def create_access_token(self, data:dict, expires_delta: timedelta) -> str:
Decoding the token, token – the token itself
def decode_token(self,token:str):
Issuing a token during authorization; for this example, we will record email and password in the token; you can select another set of data.
def login_for_access_token(self,email:str, password:str) -> Token:
Data validation. We check the existence of an entry with this login and password.
def validate_user(self, email:str, password: str) -> Union[UserDTO, bool]:
Issuing a token
The token is issued when the user is authorized, so let’s write the logic for
login_for_access_token(). Here I check for existence by users with the entered data, if it does not exist, I raise an error. Then I create a token with a certain lifetime. The lifetime can be stored in a settings file for easy modification.
def login_for_access_token(self, email: str, password: str) -> Token:
user: UserDTO = self.validate_user(email, password) #проверка введенных данных
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=15) #время действия токена
#данные для кодирования
access_token = self.create_access_token(
data={"email": user.email, "password": user.password},
expires_delta=access_token_expires
) #создание токена
return Token(access_token=access_token, token_type="bearer", access_token_expires=str(access_token_expires))
For a convenient token representation I use pydantic scheme with several fields:
access_token – the token itself
token_type – type of token
access_token_expires – duration of validity
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
access_token_expires: str
User Validation
To check the received data, I wrote a simple method with a simple check, where I access the database and check whether an entry with the same email and password exists. I'm not complicating this example by hashing the data, but you could do it:
def validate_user(self, email: str, password: str) -> Union[UserDTO, bool]:
user: UserDTO = user_repository.select_user_by_email(email)
if user and user.password.__eq__(password):
return user
else:
return False
UserDTO – pydantic schema that consists of several fields
from pydantic import BaseModel
class UserDTO(BaseModel):
id: int
login: str
email: str
password: str
Creating a token
At the last step, we checked the transmitted data, now we need to encode this data into our token and add the duration of the action. To do this, we’ll write the body of create_access_token():
def create_access_token(self, data: dict, expires_delta: timedelta) -> str:
to_encode = data.copy() #копируем данные для кодирования
expire = datetime.now(timezone.utc) + expires_delta
to_encode.update({"exp": expire}) #к текущему времени прибавляем время жизни
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm="HS256")
return encoded_jwt
When creating a token, you must specify a secret key, which can be obtained using this command:
rand openssl -hex 32
For example, you can use this key:
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
You also need to specify the signature algorithm used, in my case it is “HS256”. It is also convenient to put this data into a configuration file.
Verifying the received token
At this step, we have already checked the data and issued a token; now all that remains is to receive and check the token we issued during subsequent user requests.
In my project I do this as a separate request. To do this, we implement decode_token_and_get_token():
def get_current_user(self, token: str):
#заранее подготовим исключение
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# декодировка токена
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
#данные из токена
email: str = payload.get("email")
password: str = payload.get("password")
exp: str = payload.get("exp")
#если в токене нет поля email
if email is None:
raise credentials_exception
#если время жизни токена истекло
if datetime.fromtimestamp(float(exp)) - datetime.now() < timedelta(0):
raise credentials_exception
except InvalidTokenError:
raise credentials_exception
#проверка данных
user: UserDTO = self.validate_user(email, password)
if user is None:
raise credentials_exception
return user
When decoding a token, we must specify the token, the secret key and the algorithm we used to encode it. After decoding, we have a dictionary from which you can take the necessary data.
Creating Routers
To access our application, we will write several routers. I created a separate routers. With this approach, do not forget to add a new router to the main file.
#создаем новый роутер
router = APIRouter(
prefix="/users_api",
tags=["Users"],
)
#экземпляр класса
user_auth = UserAuth()
We will need two routers – login() for authorization and read_me() for checking the token.
@router.post("/login")
async def login(user: UserLogin):
#получаем токен и возращаем клиенту
token = user_auth.login_for_access_token(user.email, user.password)
return token
@router.post("/me")
async def read_me(token: TokenGet):
#декодируем токен и получаем обьект пользователя
return user_auth.decode_token(token.token)
TokenGet – pydantic scheme for convenient display with one token field
To view the result, you can run our application using Uvicorn
uvicorn src.main:app --use-colors --log-level ёdebug --reload
If we go to the documentation of our application, we will see our methods:
I want to say right away that when accessing an application from outside, a Cors policy error may occur. To solve this, you need to specify the following in the main application file:
app = FastAPI()
app.include_router(user_router)
#указываем адреса, которые могут обращаться к нашему приложению
origins = [
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:5174",
"http://127.0.0.1:5174",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
This ends the server part.
Client side
On the client I'm using React JS and the Vite project builder.
Working with a token
First, let's create the Auth.jsx file. In it we implement the logic of saving and receiving a token. We will store the token in localStorage under the 'Token' key
export const setToken = (token) => {
localStorage.setItem('Token', token)
}
get the saved token from localStorage
export const getToken = () => {
return localStorage.getItem('Token')
}
We send the token and receive the client data. To do this, we send a request to our server; the address can be found in the FastApi docs of your application. We indicate our token in the body of the request and wait for the result.
export async function getUserByToken(token) {
var ans = false;
const res = await axios.post(`http://127.0.0.1:8000/users_api/me`, {
"token": token
}).then((resp) => {
if (resp.status === 200) {
const response = resp.data
ans = response
}else{
ans = false
}
}).catch((error) => console.error(error));
return ans
}
User page
Let's take a fictitious user page that we want to display personalized. To do this, when the page loads, I access our storage for a token, which I send to the server
//мпортируем методы из созданного нами фаила на прошлом шаге
import {getUserByToken, getToken, setToken} from "../../Auth.jsx"
//переменная для сохранения данных пользователя
const [userData, setUserData] = useState()
// используем UseEffect, чтобы запросить данные при загрузке страницы
useEffect(() => {
let token = getToken()
if (token) {
let user = getUserByToken(token)
user.then(function(result) {
user = result
setUserData(user)
})
}
}, []);
So we sent the token, received the user data and stored it in our state.
To check, you can use a small example page:
return (
<>
{userData ? (
<div>пользователь {userData.id}</div>
) :(
<div>данные не получены</div>
)
}
</>
We wait for a response and display the page.
Conclusion
Thank you to everyone who read this article to the end, I really appreciate it. This solution is quite easy to implement and is suitable for pet projects. Of course, it can be improved and I will do this in the future. Thanks everyone.