Skip to content

Commit 2e27874

Browse files
committed
Configured JWT Authentication using fastapi-jwt-auth
1 parent 9a4724d commit 2e27874

14 files changed

+187
-33
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,3 @@ dmypy.json
167167

168168
# Generated by Windows
169169
Thumbs.db
170-

api/endpoints/items.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from typing import List
2-
from fastapi import (
3-
APIRouter,
4-
Depends,
5-
)
2+
3+
from fastapi import APIRouter, Depends
4+
from fastapi_jwt_auth import AuthJWT
65

76
import schemas
87
import crud
@@ -21,6 +20,7 @@ def create_item_for_user(user_id: int, item: schemas.ItemCreate):
2120

2221

2322
@router.get("/items/", response_model=List[schemas.Item], dependencies=[Depends(get_db)])
24-
def read_items(skip: int = 0, limit: int = 100):
23+
def user_items(Authorize: AuthJWT = Depends(), skip: int = 0, limit: int = 100):
24+
Authorize.jwt_required()
2525
items = crud.get_items(skip=skip, limit=limit)
2626
return items

api/endpoints/users.py

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,83 @@
11
import time
2+
from datetime import timedelta
23
from typing import List
34

4-
from fastapi import (
5-
APIRouter,
6-
HTTPException,
7-
Depends,
8-
)
5+
from fastapi import APIRouter, Depends, HTTPException
6+
from fastapi_jwt_auth import AuthJWT
97

108
import schemas
119
import crud
10+
from core import settings
1211
from db.utils import get_db
1312

1413
router = APIRouter()
1514
sleep_time = 10
1615

1716

17+
@router.post('/login/')
18+
def login(user_in: schemas.UserLogin, Authorize: AuthJWT = Depends()):
19+
db_user = crud.authenticate_user(
20+
email=user_in.email, password=user_in.password
21+
)
22+
if not db_user:
23+
raise HTTPException(status_code=400, detail="Incorrect email or password!")
24+
25+
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
26+
refresh_token_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
27+
28+
access_token = Authorize.create_access_token(
29+
subject=db_user.email,
30+
expires_time=access_token_expires,
31+
algorithm=settings.AUTH_JWT_TOKEN_ALGORITHM
32+
)
33+
refresh_token = Authorize.create_refresh_token(
34+
subject=db_user.email,
35+
expires_time=refresh_token_expires,
36+
algorithm=settings.AUTH_JWT_TOKEN_ALGORITHM
37+
)
38+
39+
Authorize.set_access_cookies(access_token)
40+
Authorize.set_refresh_cookies(refresh_token)
41+
42+
return {"msg": "Successfully logged in.", "access_token": access_token}
43+
44+
45+
@router.get('/user/', response_model=schemas.User, dependencies=[Depends(get_db)])
46+
def user(Authorize: AuthJWT = Depends()):
47+
Authorize.jwt_required()
48+
49+
auth_user_email = Authorize.get_jwt_subject()
50+
db_user = crud.get_user_by_email(auth_user_email)
51+
52+
return db_user
53+
54+
55+
@router.post('/refresh/')
56+
def refresh(Authorize: AuthJWT = Depends()):
57+
Authorize.jwt_refresh_token_required()
58+
59+
auth_user_email = Authorize.get_jwt_subject()
60+
61+
new_access_token = Authorize.create_access_token(subject=auth_user_email)
62+
Authorize.set_access_cookies(new_access_token)
63+
64+
return {"msg": "The token has been refreshed.", "access_token": new_access_token}
65+
66+
67+
@router.delete('/logout/')
68+
def logout(Authorize: AuthJWT = Depends()):
69+
Authorize.jwt_required()
70+
71+
Authorize.unset_jwt_cookies()
72+
return {"msg": "Successfully logged out."}
73+
74+
1875
@router.post("/users/", response_model=schemas.User, dependencies=[Depends(get_db)])
19-
def create_user(user: schemas.UserCreate):
20-
db_user = crud.get_user_by_email(email=user.email)
76+
def create_user(user_in: schemas.UserCreate):
77+
db_user = crud.get_user_by_email(email=user_in.email)
2178
if db_user:
22-
raise HTTPException(status_code=400, detail="Email already registered")
23-
return crud.create_user(user=user)
79+
raise HTTPException(status_code=400, detail="The user with this email already exists in the system!", )
80+
return crud.create_user(obj_in=user_in)
2481

2582

2683
@router.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)])

core/config.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77

88
class Settings:
99
API_V1_STR: str = "/api/v1"
10-
IPDATA_API_KEY: str = env("IPDATA_API_KEY", default="test") # Your Key Here
10+
11+
IPDATA_API_KEY: str = env("IPDATA_API_KEY", default="test")
12+
AUTH_JWT_SECRET_KEY: str = env("AUTH_JWT_SECRET_KEY", default="secret")
13+
AUTH_JWT_TOKEN_ALGORITHM = "HS256"
14+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1
15+
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7
16+
# 60 minutes * 24 hours * x days = x days
1117

1218
# Database Settings
1319
DATABASE_NAME: str = env("MYSQL_DATABASE", default="mysql_db")
1420
DATABASE_USER: str = env("MYSQL_USER", default="mysql")
1521
DATABASE_PASSWORD: str = env("MYSQL_PASSWORD", default="mysql")
22+
DATABASE_ROOT_PASSWORD: str = env("MYSQL_ROOT_PASSWORD", default="mysql")
1623
DATABASE_HOST: str = "db"
1724
DATABASE_PORT: int = env.int("MYSQL_PORT", default=3306)
1825

core/security.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional
2+
3+
from fastapi import Cookie
4+
from passlib.context import CryptContext
5+
6+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7+
8+
9+
def verify_password(plain_password: str, hashed_password: str) -> bool:
10+
return pwd_context.verify(plain_password, hashed_password)
11+
12+
13+
def get_password_hash(password: str) -> str:
14+
return pwd_context.hash(password)
15+
16+
17+
async def get_access_token_from_cookie(csrf_access_token: Optional[str] = Cookie(None)):
18+
return csrf_access_token
19+
20+
21+
async def get_refresh_token_from_cookie(csrf_refresh_token: Optional[str] = Cookie(None)):
22+
return csrf_refresh_token
23+

crud/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
get_user,
88
get_user_by_email,
99
get_users,
10-
create_user
10+
create_user,
11+
authenticate_user,
1112
)

crud/crud_user.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import schemas
22
from db import models
3+
from core.security import pwd_context, verify_password, get_password_hash
34

45

56
def get_user(user_id: int):
@@ -14,8 +15,21 @@ def get_users(skip: int = 0, limit: int = 100):
1415
return list(models.User.select().offset(skip).limit(limit))
1516

1617

17-
def create_user(user: schemas.UserCreate):
18-
fake_hashed_password = user.password + "notreallyhashed"
19-
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
18+
def create_user(obj_in: schemas.UserCreate):
19+
db_user = models.User(
20+
email=obj_in.email,
21+
hashed_password=get_password_hash(obj_in.password),
22+
fullname=obj_in.fullname,
23+
is_superuser=obj_in.is_superuser,
24+
)
2025
db_user.save()
2126
return db_user
27+
28+
29+
def authenticate_user(email: str, password: str):
30+
user = get_user_by_email(email=email)
31+
if not user:
32+
return None
33+
if not verify_password(password, user.hashed_password):
34+
return None
35+
return user

db/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ class User(peewee.Model):
66
email = peewee.CharField(unique=True, index=True)
77
hashed_password = peewee.CharField()
88
is_active = peewee.BooleanField(default=True)
9+
is_superuser = peewee.BooleanField(default=False)
10+
fullname = peewee.CharField(null=True)
911

1012
class Meta:
1113
database = mysql_db
@@ -17,4 +19,4 @@ class Item(peewee.Model):
1719
owner = peewee.ForeignKeyField(User, backref="items")
1820

1921
class Meta:
20-
database = mysql_db
22+
database = mysql_db

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ services:
66
command: bash -c "python main.py"
77
environment:
88
- IPDATA_API_KEY=test
9+
- AUTH_JWT_SECRET_KEY=test
910
volumes:
1011
- .:/code
1112
ports:

main.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
import uvicorn
2-
from fastapi import FastAPI
32

4-
from core import settings
3+
from fastapi import FastAPI, Request
4+
from fastapi.responses import JSONResponse
5+
from fastapi_jwt_auth import AuthJWT
6+
from fastapi_jwt_auth.exceptions import AuthJWTException
57

8+
from core import settings
69
from db import models
710
from db.init_db import mysql_db
8-
from api import api_router
911

12+
from api import api_router
13+
from schemas.auth_jwt_config import Settings
1014

1115
mysql_db.connect()
1216
mysql_db.create_tables([models.User, models.Item])
1317
mysql_db.close()
1418

15-
1619
app = FastAPI(
1720
openapi_url=f"{settings.API_V1_STR}/openapi.json"
1821
)
22+
23+
24+
@AuthJWT.load_config
25+
def get_config():
26+
return Settings()
27+
28+
29+
@app.exception_handler(AuthJWTException)
30+
def auth_jwt_exception_handler(request: Request, exc: AuthJWTException):
31+
return JSONResponse(
32+
status_code=exc.status_code,
33+
content={"detail": exc.message}
34+
)
35+
36+
1937
app.include_router(api_router, prefix=settings.API_V1_STR)
2038

2139

requirements.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
fastapi~=0.73.0
22
uvicorn~=0.17.3
33
peewee~=3.14.8
4-
pydantic~=1.9.0
4+
pydantic[email]~=1.9.0
55
contextvars~=2.4.0
66
pymysql~=1.0.2
77
cryptography~=36.0.1
88
environs~=9.5.0
9-
aiohttp~=3.8.1
9+
aiohttp~=3.8.1
10+
fastapi-jwt-auth~=0.5.0
11+
passlib[bcrypt]~=1.7.4

schemas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
)
55

66
from .user import (
7+
UserBase,
8+
UserLogin,
79
UserCreate,
810
User
911
)

schemas/auth_jwt_config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pydantic import BaseModel
2+
from core import settings
3+
4+
5+
class Settings(BaseModel):
6+
authjwt_secret_key: str = settings.AUTH_JWT_SECRET_KEY
7+
authjwt_token_location: set = {"cookies"}
8+
authjwt_cookie_secure: bool = False # Only allow JWT cookies to be sent over https
9+
authjwt_cookie_csrf_protect: bool = False # Change it on production

schemas/user.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,42 @@
1-
from typing import List
2-
from pydantic import BaseModel
1+
from typing import List, Optional
2+
from pydantic import BaseModel, EmailStr
33

44
from .utils import PeeweeGetterDict
55
from .item import Item
66

77

88
class UserBase(BaseModel):
9-
email: str
9+
email: Optional[EmailStr] = None
10+
is_active: Optional[bool] = True
11+
is_superuser: bool = False
12+
fullname: Optional[str] = None
13+
14+
15+
class UserLogin(BaseModel):
16+
email: EmailStr
17+
password: str
1018

1119

1220
class UserCreate(UserBase):
21+
email: EmailStr
1322
password: str
23+
fullname: Optional[str] = None
1424

1525

16-
class User(UserBase):
17-
id: int
18-
is_active: bool
26+
class UserInDBBase(UserBase):
27+
id: Optional[int] = None
1928
items: List[Item] = []
2029

2130
class Config:
2231
orm_mode = True
2332
getter_dict = PeeweeGetterDict
33+
34+
35+
# Additional properties to return via API
36+
class User(UserInDBBase):
37+
pass
38+
39+
40+
# Additional properties stored in DB
41+
class UserInDB(UserInDBBase):
42+
hashed_password: str

0 commit comments

Comments
 (0)