Integrate FastAPI with authentication using JWT
- Tram Ho
FastAPI JWT
Login with json-web-token in Fastapi
Intro: Quick guide to setup login with JWT in Fastapi
Thinking: With the usual serverless mechanism, the steps of user authentication in a service backend via API usually take place as follows:
- User provides username + password and calls API login to get authentication code (JWT token)
- User uses the JWT authentication code provided by the system, add this code in the header of each request so that the system checks every time the API calls.
As we all know or on the homepage of FastAPI wrote
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
Here I will write a quick setup to install using JWT in FastAPI. Mainly we have 2 steps:
- Write a Login API to get JWT token
- Write an API to get any data, JWT token is required to get data
Step1: Setup a FastAPI service
First, install fastapi and create the file main.py and add a few lines of code to make sure FastAPI works
1 2 3 4 | # Terminal CLI $ pip install fastapi uvicorn |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # File main.py import uvicorn from fastapi import FastAPI app = FastAPI( title='FastAPI JWT', openapi_url='/openapi.json', docs_url='/docs', description='fastapi jwt' ) @app.post('/login') def login(): return 'Success' @app.get('/books') def list_books(): return {'data': ['Sherlock Homes', 'Harry Potter', 'Rich Dad Poor Dad']} if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0', port=8000) |
At the CLI interface, run command
1 2 | $ uvicorn main:app --reload |
Open a web browser to check http://localhost:8000/docs
Call to try the API to see if it works
Step2: Create login form
Using Pydantic to create a login form, now the login form only needs 2 fields: username and password.
- More classes LoginRequest
- Add request_data to login function & use print to show request_data on CLI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # File main.py ... from pydantic import BaseModel app = FastAPI( title='FastAPI JWT', openapi_url='/openapi.json', docs_url='/docs', description='fastapi jwt' ) class LoginRequest(BaseModel): username: str password: str @app.post('/login') def login(request_data: LoginRequest): print(f'[x] request_data: {request_data.__dict__}') return 'Success' ... |
Check the webrower, the request body already shows the form
Click Try it out, try entering username & password: test/test and check CLI
Step3: function verify_password
Enter username, password of course will need a function to check if username/password is correct, write a simple function to check username and password is equal to admin/admin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # File main.py import uvicorn from fastapi import FastAPI, HTTPException from pydantic import BaseModel ... def verify_password(username, password): if username == 'admin' and password == 'admin': return True return False @app.post('/login') def login(request_data: LoginRequest): print(f'[x] request_data: {request_data.__dict__}') if verify_password(username=request_data.username, password=request_data.password): return 'Success' else: raise HTTPException(status_code=404, detail="User not found") ... |
The logic is if username/password = admin/admin then return Success, otherwise return 404 – User not found
Test the case not found
Step4: Generate & return token
When entering correct username/password, login api needs to return JWT token, so now we write gentoken
- Install PyJWT, to generate jwt token, we need to use PyJWT library. Open CLI and run:
1 2 | $ pip install PyJWT |
- Create functions generate_token, return value of this function instead of return ‘Success’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | # File main.py import jwt import uvicorn from datetime import datetime, timedelta from typing import Union, Any from fastapi import FastAPI, HTTPException from pydantic import BaseModel SECURITY_ALGORITHM = 'HS256' SECRET_KEY = '123456' ... def generate_token(username: Union[str, Any]) -> str: expire = datetime.utcnow() + timedelta( seconds=60 * 60 * 24 * 3 # Expired after 3 days ) to_encode = { "exp": expire, "username": username } encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=SECURITY_ALGORITHM) return encoded_jwt @app.post('/login') def login(request_data: LoginRequest): print(f'[x] request_data: {request_data.__dict__}') if verify_password(username=request_data.username, password=request_data.password): token = generate_token(request_data.username) return { 'token': token } else: raise HTTPException(status_code=404, detail="User not found") ... |
Try calling the API and see the result
The result is a token chain consisting of 3 parts
Step5: Required header Token when calling API books
To add token input form in Swagger and check required token, FastAPi has built-in utility lib, HTTPBearer.
- In security.py, to add reusable_oauth2 is an instance of HTTPBearer
- Use reusable_oauth2 as dependencies in the books . API
1 2 3 4 5 6 7 8 | # File security.py from fastapi.security import HTTPBearer reusable_oauth2 = HTTPBearer( scheme_name='Authorization' ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | # File main.py ... from security import validate_token, reusable_oauth2 ... @app.get('/books', dependencies=[Depends(reusable_oauth2)]) def list_books(): return {'data': ['Sherlock Homes', 'Harry Potter', 'Rich Dad Poor Dad']} ... |
Back on the webrowser, you can see the lock icon and the Authorize button in the upper right corner
Call to try API /books without entering token, will see response "detail": "Not authenticated"
Step6: validate_token
After briefly understanding the use of HTTPBearer, we use it to get token and check validity
- In security.py add function validate_token
- Use validate_token as dependencies in the books . API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | # File security.py from datetime import datetime import jwt from fastapi import Depends, HTTPException from fastapi.security import HTTPBearer from pydantic import ValidationError SECURITY_ALGORITHM = 'HS256' SECRET_KEY = '123456' reusable_oauth2 = HTTPBearer( scheme_name='Authorization' ) def validate_token(http_authorization_credentials=Depends(reusable_oauth2)) -> str: """ Decode JWT token to get username => return username """ try: payload = jwt.decode(http_authorization_credentials.credentials, SECRET_KEY, algorithms=[SECURITY_ALGORITHM]) if payload.get('username') < datetime.now(): raise HTTPException(status_code=403, detail="Token expired") return payload.get('username') except(jwt.PyJWTError, ValidationError): raise HTTPException( status_code=403, detail=f"Could not validate credentials", ) |
1 2 3 4 5 6 7 8 9 10 11 12 | # File main.py ... from security import validate_token ... @app.get('/books', dependencies=[Depends(validate_token)]) def list_books(): return {'data': ['Sherlock Homes', 'Harry Potter', 'Rich Dad Poor Dad']} ... |
Go back to the webrowser, call API login to get the token and enter that token to call API /books and see how it goes.
Conclusion
Integrating JWT into FastAPI is quite simple, the important thing is that we need to know how to use it HTTPBearer and dependencies are the built-in tools provided by the framework.
Also, if possible, you can learn more about how lib works PyJWT in https://pyjwt.readthedocs.io/en/stable/
Run this project
To run this project:
1 2 3 4 5 6 | $ git clone https://github.com/Longdh57/fastapi-jwt.git $ cd fastapi-jwt $ virtualenv -p python3 .venv $ source .venv/bin/active $ pip install -r requirements.txt $ uvicorn main:app --reload |