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:

  1. 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:
  1. Decoding the token, token – the token itself

def decode_token(self,token:str):
  
  1. 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:
  1. 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:

to log in and issue a token

to log in and issue a token

to send and verify the token

to send and verify the token

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *