/
šŸ§‘šŸ»ā€šŸ«
Blacklist and Refresh Tokens (JWT) with FastAPI (Part 3)
Search
Duplicate
Try Notion
šŸ§‘šŸ»ā€šŸ«
Blacklist and Refresh Tokens (JWT) with FastAPI (Part 3)
This guide is a follow up toĀ Use Google Login (OAuth) with FastAPI and JWT, in the previous guide the added to ourĀ FastAPIĀ applicationĀ JWTĀ support.
Now we are going to improve this tokens and create a blacklist token to ban tokens in case is necessary or just to invalidate a token after a user logs out. Also we are going to create refresh tokens to avoid requesting the user credentials after each access token expires.
Requirements
This guide assumes you already have installed in your systemĀ python3.8 (or newer).
You have your the Google Credentials created and theĀ loginĀ andĀ authĀ endpoint defined. Following the guide:Ā Use Google Login (OAuth) with FastAPI - Python.
You have support forĀ JWT, defined in the guideĀ Use Google Login (OAuth) with FastAPI and JWT.
Note: if you donā€™t want to read the last two guides, you can just clone theĀ guide-2Ā branch to follow along.
git clone -b guide-2 https://github.com/hanchon-live/tutorial-fastapi-oauth.git cd tutorial-fastapi-oauth # Create the virtualenv, activate it, and install the requirements python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
Shell
Blacklist tokens
Letā€™s start with aĀ logoutĀ endpoint to allow the user to leave our application without the fear of anyone else in the same device using its token.
This endpoint is going to set the token as invalid. In order to do this, we are going to add the token to a list of blacklisted tokens.
This change make us also validate, for each request, if the token is not in the list of blacklisted tokens.
Validate the token and return it:
Our functionĀ get_current_user_emailĀ returns just the user email, but we are going to need the token to add it to the blacklist database (on theĀ /logoutĀ endpoint).
So we are going to create a new functionĀ get_current_user_tokenĀ that returns theĀ tokenĀ instead of theĀ email.
Create theĀ get_current_user_tokenĀ function onĀ apps/jwt.py:
async def get_current_user_token(token: str = Depends(oauth2_scheme)): _ = get_current_user_email(token) return token
Python
apps/jwt.py
Note: this works because if the credentials are invalid, theĀ get_current_user_emailĀ function will raise anĀ Exception.
Create a blacklist tokens database:
To avoid setting up a database for this example, we are just going to just use a text file.
Letā€™s create a new fileĀ apps/db.py file and add the fake database.
def init_blacklist_file(): open('blacklist_db.txt', 'a').close() return True def add_blacklist_token(token): with open('blacklist_db.txt', 'a') as file: file.write(f'{token},') return True def is_token_blacklisted(token): with open('blacklist_db.txt') as file: content = file.read() array = content[:-1].split(',') for value in array: if value == token: return True return False
Python
apps/db.py
The file should to be created (if needed) when the application starts, so we are going to add theĀ init_backlist_fileĀ to theĀ ./main.pyĀ file:
from apps.db import init_blacklist_file if __name__ == '__main__': init_blacklist_file() uvicorn.run(app, port=7000)
Python
./main.py
Create the logout endpoint:
We are going to add the endpoint to theĀ main.pyĀ file, we just need to callĀ DependsĀ on the newly createdĀ get_current_user_tokenĀ function.
If the token is valid, we are going to just add it to the blacklist.
from fastapi import Depends from apps.jwt import get_current_user_token from apps.jwt import CREDENTIALS_EXCEPTION from apps.db import add_blacklist_token from fastapi.responses import JSONResponse @app.get('/logout') def logout(token: str = Depends(get_current_user_token)): if add_blacklist_token(token): return JSONResponse({'result': True}) raise CREDENTIALS_EXCEPTION
Python
main.py
Add the Logout button:
Add theĀ LogoutĀ button to the end of theĀ /tokenĀ endpoint (in theĀ main.pyĀ file), theĀ JWTĀ must be sent in this message.
<button onClick='fetch("http://127.0.0.1:7000/logout",{ headers:{ "Authorization": "Bearer " + window.localStorage.getItem("jwt") }, }).then((r)=>r.json()).then((msg)=>{ console.log(msg); if (msg["result"] === true) { window.localStorage.removeItem("jwt"); } });'> Logout </button>
Markup
main.py
Note: we are going to delete fromĀ localStorageĀ theĀ JWTĀ value if the logout function was successful, this is useful in case we want to render another element when the user is no longer logged in.
Use blacklist token list on the validation:
Now that we supportĀ blacklistĀ tokens, theĀ get_current_user_emailĀ function needs to check if the token is NOT in the blacklist token list.
So letā€™s modify the apps/jwt.py file and edit theĀ get_current_user_emailĀ function to check if the token is blacklisted:
from apps.db import is_token_blacklisted async def get_current_user_email(token: str = Depends(oauth2_scheme)): if is_token_blacklisted(token): raise CREDENTIALS_EXCEPTION ...
Python
apps/jwt.py
Test the blacklisted tokens
Now we can test everything:
Run the app and go toĀ http://127.0.0.1:7000/.
Log in with your Google Credentials.
Generate theĀ JWTĀ (this button stores the token onĀ localStorage).
Call the protected API call and it should work.
Press the Logout button.
A new line will be added to theĀ blacklist_db.txt file.
TheĀ JWTĀ variable is removed from yourĀ localStorage.
Manually add theĀ JWTĀ var to your browser.
Look for the token in yourĀ blacklist_db.txtĀ file. (Ignore theĀ ,)
Press F12 in your browser, go toĀ applicationĀ and add theĀ jwtĀ variable with the value of your token.
Try to use the protected API call and it should fail.
With all this changes we have the blacklist token functionality complete. So letā€™s improve the application with refresh tokens.
Refresh Tokens
A refresh token is a special token that will allow the users to create new tokens when the one that they are using expires.
It works the same way as theĀ access tokenĀ that we are creating on the login function, but this token expiration time is a much longer time, for example, 30 days.
In the frontend the user will try to make a request using theĀ JWTĀ token that is stored in the browserā€™sĀ localStorageĀ and if the server returns a credentials error, it will try to generate a newĀ JWTĀ token using theĀ refresh tokenĀ and make the request again.
The refresh token can be sent in the request header or as a post body, we are going to sent it as post body to follow theĀ OAuth2 documentation.
Create a refresh token:
We are going to add a function to create a token with an expiration time of 30 days.
Add to the fileĀ apps/jwt.pyĀ theĀ create_refresh_tokenĀ function:
REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 30 def create_refresh_token(email): expires = timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES) return create_access_token(data={'sub': email}, expires_delta=expires)
Python
apps/jwt.py
Add to theĀ /auth/tokenĀ route the refresh token (apps/auth.py):
from apps.jwt import create_refresh_token @auth_app.route('/token') async def auth(request: Request): ... return JSONResponse({ 'result': True, 'access_token': create_token(user_data['email']), 'refresh_token': create_refresh_token(user_data['email']), })
Python
apps/auth.py
Save theĀ refresh_tokenĀ on the browserā€™sĀ localStorageĀ when theĀ /tokenĀ endpoint (in the main.pyĀ file) is called:
@app.get('/token') async def token(request: Request): return HTMLResponse(''' ... window.localStorage.setItem('jwt', req.response["access_token"]); window.localStorage.setItem('refresh', req.response["refresh_token"]); ...
Python
main.py
Refactor the token decoder:
In theĀ apps/jwt.pyĀ file letā€™s create the functionĀ decode_token:
def decode_token(token): return jwt.decode(token, API_SECRET_KEY, algorithms=[API_ALGORITHM])
Python
apps/jwt.py
And letā€™s call it inside theĀ get_current_user_emailĀ function:
# Change this line: # jwt.decode(token, API_SECRET_KEY, algorithms=[API_ALGORITHM]) # to this: payload = decode_token(token)
Python
apps/jwt.py file
Now we have the decode token function ready to be called when we want to validate the refresh token.
Create the refresh endpoint:
This endpoint will read the POST data, check if itā€™s a refresh token request, validate the token and get its payload.
With this information we will check if the token is expired and if the email is in our database. After this validations, we create a newĀ access_tokenĀ and return it to the user.
If there is an error in any step, we just return a credentials exception.
Letā€™s add the endpoint to theĀ apps/auth.pyĀ file:
from apps.jwt import decode_token from datetime import datetime @auth_app.post('/refresh') async def refresh(request: Request): try: # Only accept post requests if request.method == 'POST': form = await request.json() if form.get('grant_type') == 'refresh_token': token = form.get('refresh_token') payload = decode_token(token) # Check if token is not expired if datetime.utcfromtimestamp(payload.get('exp')) > datetime.utcnow(): email = payload.get('sub') # Validate email if valid_email_from_db(email): # Create and return token return JSONResponse({'result': True, 'access_token': create_token(email)}) except Exception: raise CREDENTIALS_EXCEPTION raise CREDENTIALS_EXCEPTION
Python
apps/auth.py
Add a button to refresh the JWT
To test the new functionality letā€™s add a refresh token call on theĀ /tokenĀ endpoint in the fileĀ main.py
<button onClick='fetch("http://127.0.0.1:7000/auth/refresh",{ method: "POST", headers:{ "Authorization": "Bearer " + window.localStorage.getItem("jwt") }, body:JSON.stringify({ grant_type:\"refresh_token\", refresh_token:window.localStorage.getItem(\"refresh\") }) }).then((r)=>r.json()).then((msg)=>{ console.log(msg); if (msg["result"] === true) { window.localStorage.setItem("jwt", msg["access_token"]); } });'> Refresh </button>
Markup
main.py
This function will send the token to the backend and will store the newĀ JWTĀ value on theĀ localStorage.
Link to the code
This app is uploaded to github, you can view the repository using thisĀ link, this tutorial is the branchĀ guide-3
Related Guides
TheĀ part 1Ā of this tutorial explains how to create a Google Application, and how to integrate the Google OAuth with ourĀ FastAPIĀ project.
TheĀ part 2Ā of this tutorial explains how to create sub-applications withĀ FastAPI. It explains how to configure differentĀ middlewaresĀ and how to create and useĀ JWTĀ Bearer token authentication for each protected endpoints.