Skip to content

Commit 6ff598c

Browse files
committed
startup checks and moved tfa verify to dependency
1 parent 9a0516f commit 6ff598c

File tree

8 files changed

+85
-23
lines changed

8 files changed

+85
-23
lines changed

docs/two_factor_flowchart.drawio

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<mxfile host="Electron" modified="2022-11-26T15:56:47.173Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.3.0 Chrome/104.0.5112.114 Electron/20.1.3 Safari/537.36" etag="Ya5MVrSHKQGV9EB_zEQE" version="20.3.0" type="device" pages="2"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram><diagram id="mogvwWM-gPOIvBLYb1fR" name="Page-2">ddG9DoIwEADgp+le2xjdEXVxYnBu6EmbFI6UM6BPL6QFbNCp1+/u+stkVg8Xr1pzQw2OCa4HJk9MiN2Oi3GY5BXkuD8EqLzVsWiFwr4hIo/6tBq6pJAQHdk2xRKbBkpKTHmPfVr2QJfu2qoKNlCUym31bjWZ+RZ89SvYytBy4Zip1VwcoTNKY/9FMmcy84gUonrIwE2PN79L6Dv/yS4H89DQj4YxWNceJ8kPyfwD</diagram></mxfile>

fastapi_2fa/api/deps/db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ async def get_db() -> Generator:
1515
finally:
1616
print('closing db...')
1717
await db.close()
18+
print('..db closed!')

fastapi_2fa/api/deps/users.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from fastapi_2fa.api.deps.db import get_db
88
from fastapi_2fa.core.config import settings
9+
from fastapi_2fa.core.two_factor_auth import verify_token
910
from fastapi_2fa.crud.users import user_crud
1011
from fastapi_2fa.models.users import User
1112
from fastapi_2fa.schemas.jwt_token_schema import JwtTokenPayload
@@ -69,3 +70,17 @@ async def get_authenticated_user_pre_tfa(
6970
"and validate TFA token within "
7071
f"{settings.PRE_TFA_TOKEN_EXPIRE_MINUTES} minutes",
7172
)
73+
74+
75+
async def get_authenticated_tfa_user(
76+
tfa_token: str,
77+
db: Session = Depends(get_db),
78+
user: User = Depends(get_authenticated_user_pre_tfa)
79+
) -> User:
80+
if verify_token(user=user, token=tfa_token):
81+
return user
82+
83+
raise HTTPException(
84+
status_code=status.HTTP_403_FORBIDDEN,
85+
detail="TOTP token mismatch"
86+
)

fastapi_2fa/api/endpoints/api_v1/auth.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@ async def signup(
4242
db: Session = Depends(get_db), user_data: UserCreate = Depends()
4343
) -> Any:
4444
try:
45-
# async with db.begin_nested():
4645
user = await user_crud(transaction=True).create(
4746
db=db,
4847
user=user_data,
4948
)
50-
# raise Exception
5149
if user_data.tfa_enabled:
5250
device, qr_code = await device_crud(transaction=True).create(
5351
db=db,
@@ -78,6 +76,17 @@ async def signup(
7876
summary="Create access and refresh tokens for user",
7977
status_code=status.HTTP_200_OK,
8078
response_model=JwtTokenSchema | PreTfaJwtTokenSchema,
79+
responses={
80+
200: {
81+
"description": "Credentials ok and TFA disabled",
82+
},
83+
202: {
84+
"description": "Credentials ok and TFA enabled",
85+
},
86+
401: {
87+
"description": "Invalid credentials",
88+
},
89+
}
8190
)
8291
async def login(
8392
response: Response,
@@ -91,7 +100,7 @@ async def login(
91100
# wrong credentials
92101
if not user:
93102
raise HTTPException(
94-
status_code=status.HTTP_400_BAD_REQUEST,
103+
status_code=status.HTTP_401_UNAUTHORIZED,
95104
detail="Incorrect email or password",
96105
)
97106

@@ -115,7 +124,9 @@ async def login(
115124

116125

117126
@auth_router.get(
118-
"/test-token", summary="Test if the access token is ok", response_model=UserOut
127+
"/test-token",
128+
summary="Authenticated endpoint to test if access token is ok",
129+
response_model=UserOut
119130
)
120131
async def test_token(user: User = Depends(get_authenticated_user)):
121132
"""
@@ -124,7 +135,11 @@ async def test_token(user: User = Depends(get_authenticated_user)):
124135
return user
125136

126137

127-
@auth_router.post("/refresh", summary="Refresh token", response_model=JwtTokenSchema)
138+
@auth_router.post(
139+
"/refresh",
140+
summary="Refresh token for elapsed access token",
141+
response_model=JwtTokenSchema
142+
)
128143
async def refresh_token(
129144
db: Session = Depends(get_db), refresh_token: str = Body(embed=True, title='refresh token')
130145
):

fastapi_2fa/api/endpoints/api_v1/two_factor_auth.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from sqlalchemy.orm import Session
66

77
from fastapi_2fa.api.deps.db import get_db
8-
from fastapi_2fa.api.deps.users import (get_authenticated_user,
8+
from fastapi_2fa.api.deps.users import (get_authenticated_tfa_user,
9+
get_authenticated_user,
910
get_authenticated_user_pre_tfa)
1011
from fastapi_2fa.core import security
1112
from fastapi_2fa.core.enums import DeviceTypeEnum
1213
from fastapi_2fa.core.two_factor_auth import (qr_code_from_key,
13-
verify_backup_token,
14-
verify_token)
14+
verify_backup_token)
1515
from fastapi_2fa.core.utils import send_mail_backup_tokens
1616
from fastapi_2fa.crud.backup_token import backup_token_crud
1717
from fastapi_2fa.crud.device import device_crud
@@ -32,17 +32,11 @@
3232
async def login_tfa(
3333
tfa_token: str,
3434
db: Session = Depends(get_db),
35-
user: User = Depends(get_authenticated_user_pre_tfa),
35+
user: User = Depends(get_authenticated_tfa_user),
3636
) -> Any:
37-
if verify_token(user=user, token=tfa_token):
38-
return JwtTokenSchema(
39-
access_token=security.create_jwt_access_token(user.id),
40-
refresh_token=security.create_jwt_refresh_token(user.id),
41-
)
42-
43-
raise HTTPException(
44-
status_code=status.HTTP_403_FORBIDDEN,
45-
detail="TOTP token mismatch"
37+
return JwtTokenSchema(
38+
access_token=security.create_jwt_access_token(user.id),
39+
refresh_token=security.create_jwt_refresh_token(user.id),
4640
)
4741

4842

fastapi_2fa/core/utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,26 @@ def __repr__(self) -> str:
3131

3232

3333
def send_mail_backup_tokens(user: User, device: Device) -> AsyncResult:
34-
email_ob = Email(
34+
email_obj = Email(
3535
to_=[user.email],
3636
from_=settings.FAKE_EMAIL_SENDER,
3737
text_=f"Backup tokens : {device.backup_tokens}"
3838
)
39-
task = send_email_task.apply_async(kwargs={'email': email_ob.to_json()})
39+
print(
40+
f"Sending email task to celery, email:\n\n***\n{email_obj.text_}\n***\n"
41+
)
42+
task = send_email_task.apply_async(kwargs={'email': email_obj.to_json()})
4043
return task
4144

4245

4346
def send_mail_totp_token(user: User, token: str) -> AsyncResult:
44-
email_ob = Email(
47+
email_obj = Email(
4548
to_=[user.email],
4649
from_=settings.FAKE_EMAIL_SENDER,
4750
text_=f"Access TOTP token : {token}"
4851
)
49-
task = send_email_task.apply_async(kwargs={'email': email_ob.to_json()})
52+
print(
53+
f"Sending email task to celery, email:\n\n***\n{email_obj.text_}\n***\n"
54+
)
55+
task = send_email_task.apply_async(kwargs={'email': email_obj.to_json()})
5056
return task

fastapi_2fa/crud/users.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ async def authenticate(db: Session, email: EmailStr, password: str) -> User | No
2929
user: User = await UserCrud.get_by_email(db=db, email=email)
3030
if not user:
3131
return None
32+
user = user[0]
3233
if not verify_password(password=password, hashed_password=user.hashed_password):
3334
return None
3435
return user
@@ -50,7 +51,7 @@ async def update(
5051
async def get_by_email(db: Session, email: EmailStr) -> User | None:
5152
query = select(User).where(User.email == email)
5253
model_query = await db.execute(query)
53-
(model_instance,) = model_query.first()
54+
model_instance = model_query.first()
5455
return model_instance
5556

5657

fastapi_2fa/main.py

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

55
from fastapi_2fa.api.endpoints.router import router
66
from fastapi_2fa.core.config import settings
7+
from fastapi_2fa.db.session import SessionLocal
8+
from fastapi_2fa.tasks.celery_conf import celery
79

810
app = FastAPI(
911
title=settings.PROJECT_NAME,
@@ -15,6 +17,33 @@
1517
app.include_router(router=router, prefix=settings.API_V1_STR)
1618

1719

20+
@app.on_event("startup")
21+
async def app_startup():
22+
# CHECK DB
23+
try:
24+
db = SessionLocal()
25+
await db.execute("SELECT 1")
26+
print("Database is connected")
27+
except Exception:
28+
raise RuntimeError("Problems reaching db")
29+
finally:
30+
await db.close()
31+
32+
# CHECK CELERY BROKER
33+
try:
34+
celery.broker_connection().ensure_connection(max_retries=3)
35+
except Exception as ex:
36+
raise RuntimeError("Failed to connect to celery broker, {}".format(str(ex)))
37+
print('Celery broker is running')
38+
39+
# CHECK CELERY WORKER
40+
insp = celery.control.inspect()
41+
availability = insp.ping()
42+
if not availability:
43+
raise RuntimeError('Celery worker is not running')
44+
print('Celery worker is running')
45+
46+
1847
# Set all CORS enabled origins
1948
if settings.BACKEND_CORS_ORIGINS:
2049
app.add_middleware(

0 commit comments

Comments
 (0)