├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── api.py ├── auth │ ├── __init__.py │ ├── auth_bearer.py │ └── auth_handler.py └── model.py ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | venv/ 3 | env/ 4 | .env 5 | **/__pycache__ 6 | .vscode 7 | Pipfile* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TestDriven.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Securing FastAPI with JWT Token-based Authentication 2 | 3 | ### Want to learn how to build this? 4 | 5 | Check out the [post](https://testdriven.io/blog/fastapi-jwt-auth/). 6 | 7 | ## Want to use this project? 8 | 9 | 1. Fork/Clone 10 | 11 | 1. Create and activate a virtual environment: 12 | 13 | ```sh 14 | $ python3 -m venv venv && source venv/bin/activate 15 | ``` 16 | 17 | 1. Install the requirements: 18 | 19 | ```sh 20 | (venv)$ pip install -r requirements.txt 21 | ``` 22 | 23 | 1. Run the app: 24 | 25 | ```sh 26 | (venv)$ python main.py 27 | ``` 28 | 29 | 1. Test at [http://localhost:8081/docs](http://localhost:8081/docs) 30 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0ndrec/fastapi-jwt/8af3a9a3c3e633fea5f1eee6c28f8a9a10700a20/app/__init__.py -------------------------------------------------------------------------------- /app/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Body, Depends 2 | 3 | from app.model import PostSchema, UserSchema, UserLoginSchema 4 | from app.auth.auth_bearer import JWTBearer 5 | from app.auth.auth_handler import signJWT 6 | 7 | 8 | posts = [ 9 | { 10 | "id": 1, 11 | "title": "Pancake", 12 | "content": "Lorem Ipsum ..." 13 | } 14 | ] 15 | 16 | users = [] 17 | 18 | app = FastAPI() 19 | 20 | 21 | # helpers 22 | 23 | def check_user(data: UserLoginSchema): 24 | for user in users: 25 | if user.email == data.email and user.password == data.password: 26 | return True 27 | return False 28 | 29 | 30 | # route handlers 31 | 32 | @app.get("/", tags=["root"]) 33 | async def read_root() -> dict: 34 | return {"message": "Welcome to your blog!"} 35 | 36 | 37 | @app.get("/posts", tags=["posts"]) 38 | async def get_posts() -> dict: 39 | return { "data": posts } 40 | 41 | 42 | @app.get("/posts/{id}", tags=["posts"]) 43 | async def get_single_post(id: int) -> dict: 44 | if id > len(posts): 45 | return { 46 | "error": "No such post with the supplied ID." 47 | } 48 | 49 | for post in posts: 50 | if post["id"] == id: 51 | return { 52 | "data": post 53 | } 54 | 55 | 56 | @app.post("/posts", dependencies=[Depends(JWTBearer())], tags=["posts"]) 57 | async def add_post(post: PostSchema) -> dict: 58 | post.id = len(posts) + 1 59 | posts.append(post.dict()) 60 | return { 61 | "data": "post added." 62 | } 63 | 64 | 65 | @app.post("/user/signup", tags=["user"]) 66 | async def create_user(user: UserSchema = Body(...)): 67 | users.append(user) # replace with db call, making sure to hash the password first 68 | return signJWT(user.email) 69 | 70 | 71 | @app.post("/user/login", tags=["user"]) 72 | async def user_login(user: UserLoginSchema = Body(...)): 73 | if check_user(user): 74 | return signJWT(user.email) 75 | return { 76 | "error": "Wrong login details!" 77 | } 78 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0ndrec/fastapi-jwt/8af3a9a3c3e633fea5f1eee6c28f8a9a10700a20/app/auth/__init__.py -------------------------------------------------------------------------------- /app/auth/auth_bearer.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request, HTTPException 2 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 3 | 4 | from .auth_handler import decodeJWT 5 | 6 | 7 | class JWTBearer(HTTPBearer): 8 | def __init__(self, auto_error: bool = True): 9 | super(JWTBearer, self).__init__(auto_error=auto_error) 10 | 11 | async def __call__(self, request: Request): 12 | credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request) 13 | if credentials: 14 | if not credentials.scheme == "Bearer": 15 | raise HTTPException(status_code=403, detail="Invalid authentication scheme.") 16 | if not self.verify_jwt(credentials.credentials): 17 | raise HTTPException(status_code=403, detail="Invalid token or expired token.") 18 | return credentials.credentials 19 | else: 20 | raise HTTPException(status_code=403, detail="Invalid authorization code.") 21 | 22 | def verify_jwt(self, jwtoken: str) -> bool: 23 | isTokenValid: bool = False 24 | 25 | try: 26 | payload = decodeJWT(jwtoken) 27 | except: 28 | payload = None 29 | if payload: 30 | isTokenValid = True 31 | return isTokenValid 32 | -------------------------------------------------------------------------------- /app/auth/auth_handler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Dict 3 | 4 | import jwt 5 | from decouple import config 6 | 7 | 8 | JWT_SECRET = config("secret") 9 | JWT_ALGORITHM = config("algorithm") 10 | 11 | 12 | def token_response(token: str): 13 | return { 14 | "access_token": token 15 | } 16 | 17 | 18 | def signJWT(user_id: str) -> Dict[str, str]: 19 | payload = { 20 | "user_id": user_id, 21 | "expires": time.time() + 600 22 | } 23 | token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) 24 | 25 | return token_response(token) 26 | 27 | 28 | def decodeJWT(token: str) -> dict: 29 | try: 30 | decoded_token = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) 31 | return decoded_token if decoded_token["expires"] >= time.time() else None 32 | except: 33 | return {} 34 | -------------------------------------------------------------------------------- /app/model.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, EmailStr 2 | 3 | 4 | class PostSchema(BaseModel): 5 | id: int = Field(default=None) 6 | title: str = Field(...) 7 | content: str = Field(...) 8 | 9 | class Config: 10 | schema_extra = { 11 | "example": { 12 | "title": "Securing FastAPI applications with JWT.", 13 | "content": "In this tutorial, you'll learn how to secure your application by enabling authentication using JWT. We'll be using PyJWT to sign, encode and decode JWT tokens...." 14 | } 15 | } 16 | 17 | 18 | class UserSchema(BaseModel): 19 | fullname: str = Field(...) 20 | email: EmailStr = Field(...) 21 | password: str = Field(...) 22 | 23 | class Config: 24 | schema_extra = { 25 | "example": { 26 | "fullname": "Abdulazeez Abdulazeez Adeshina", 27 | "email": "abdulazeez@x.com", 28 | "password": "weakpassword" 29 | } 30 | } 31 | 32 | class UserLoginSchema(BaseModel): 33 | email: EmailStr = Field(...) 34 | password: str = Field(...) 35 | 36 | class Config: 37 | schema_extra = { 38 | "example": { 39 | "email": "abdulazeez@x.com", 40 | "password": "weakpassword" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.run("app.api:app", host="0.0.0.0", port=8081, reload=True) 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | email-validator==1.3.1 2 | fastapi==0.89.1 3 | PyJWT==2.6.0 4 | python-decouple==3.7 5 | uvicorn==0.20.0 6 | --------------------------------------------------------------------------------