Skip to content

Commit af70033

Browse files
committed
totp token verify
1 parent d1bbb2f commit af70033

File tree

7 files changed

+36
-15
lines changed

7 files changed

+36
-15
lines changed

env/.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ TFA_TOKEN_LENGTH=6
1616
# default tolerance = 30 sec
1717
# this number is multiplied for 30 sec to increas it.
1818
# -->MAX = 10 => 5 minutes
19-
TOTP_TOKEN_DURATION=2
19+
TOTP_TOKEN_TOLERANCE=2
2020
TOTP_ISSUER_NAME=fastapi_2fa
2121

2222
ACCESS_TOKEN_EXPIRE_MINUTES=30

fastapi_2fa/api/deps/users.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ async def _get_user_from_jwt(
2020
token: str,
2121
db: Session,
2222
expire_err_message: str = "Token expired",
23-
jwt_err_message: str = "Could not validate credentials",
23+
jwt_err_message: str = "Could not validate credentials, "
24+
"if TFA is enabled, please confirm token first",
2425
):
2526
try:
2627
payload = jwt.decode(token=token, key=key, algorithms=[settings.ALGORITHM])

fastapi_2fa/api/endpoints/api_v1/auth.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
from fastapi_2fa.core.config import settings
1616
from fastapi_2fa.core.enums import DeviceTypeEnum
1717
from fastapi_2fa.core.utils import send_backup_tokens
18+
from fastapi_2fa.core.two_factor_auth import verify_token
1819
from fastapi_2fa.crud.device import device_crud
1920
from fastapi_2fa.crud.users import user_crud
2021
from fastapi_2fa.models.users import User
21-
from fastapi_2fa.schemas.token_schema import TokenPayload, TokenSchema
22+
from fastapi_2fa.schemas.token_schema import TokenPayload, TokenSchema, PreTfaTokenSchema
2223
from fastapi_2fa.schemas.user_schema import UserCreate, UserOut
2324

2425
auth_router = APIRouter()
@@ -75,7 +76,7 @@ async def signup(
7576
"/login",
7677
summary="Create access and refresh tokens for user",
7778
status_code=status.HTTP_200_OK,
78-
response_model=TokenSchema,
79+
response_model=TokenSchema | PreTfaTokenSchema,
7980
)
8081
async def login(
8182
response: Response,
@@ -96,7 +97,7 @@ async def login(
9697
# verify 2 factor authentication
9798
if user_crud.is_tfa_enabled(user=user):
9899
response.status_code = status.HTTP_202_ACCEPTED
99-
return TokenSchema(
100+
return PreTfaTokenSchema(
100101
access_token=security.create_pre_tfa_token(user.id),
101102
refresh_token=None,
102103
)
@@ -110,16 +111,24 @@ async def login(
110111

111112
@auth_router.post(
112113
"/login/tfa",
113-
summary="Verify two factor authenticazion token",
114-
response_model=UserOut,
114+
summary="Verify two factor authentication token",
115+
response_model=TokenSchema,
115116
)
116117
async def login_tfa(
117118
tfa_token: str,
118119
db: Session = Depends(get_db),
119120
user: User = Depends(get_authenticated_user_pre_tfa),
120121
) -> Any:
121-
print(f"{tfa_token}")
122-
return user
122+
if verify_token(user=user, token=tfa_token):
123+
return TokenSchema(
124+
access_token=security.create_jwt_access_token(user.id),
125+
refresh_token=security.create_jwt_refresh_token(user.id),
126+
)
127+
128+
raise HTTPException(
129+
status_code=status.HTTP_403_FORBIDDEN,
130+
detail="TOTP token mismatch"
131+
)
123132

124133

125134
@auth_router.post(

fastapi_2fa/core/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ class BaseConfig(BaseSettings):
2828
# default tolerance = 30 sec
2929
# this number is multiplied for 30 sec to increas it.
3030
# -->MAX = 10 => 5 minutes
31-
TOTP_TOKEN_DURATION: int = Field(
32-
default=os.environ.get("TOTP_TOKEN_DURATION", 2),
31+
TOTP_TOKEN_TOLERANCE: int = Field(
32+
default=os.environ.get("TOTP_TOKEN_TOLERANCE", 2),
3333
gt=0,
3434
le=10
3535
)

fastapi_2fa/core/two_factor_auth.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,20 @@ def get_fake_otp_tokens(
4646
)
4747
yield random_otp
4848

49+
50+
def verify_token(user: User, token:str) -> bool:
51+
assert user.tfa_enabled is True, 'User does not have TFA enabled'
52+
assert user.device is not None, 'User has no associated device'
53+
decoded_key = _fernet_decode(value=user.device.key)
54+
totp = pyotp.TOTP(decoded_key)
55+
result = totp.verify(token, valid_window=settings.TOTP_TOKEN_TOLERANCE)
56+
return result
57+
58+
4959
def qr_code_from_key(encoded_key: str, user_email: str):
5060
decoded_key = _fernet_decode(value=encoded_key)
5161
qrcode_key = pyotp.TOTP(
52-
decoded_key, interval=settings.TOTP_TOKEN_DURATION
62+
decoded_key, interval=settings.TOTP_TOKEN_TOLERANCE
5363
).provisioning_uri(user_email, issuer_name=settings.TOTP_ISSUER_NAME)
5464
img = qrcode.make(qrcode_key)
5565
buffer = io.BytesIO()

fastapi_2fa/models/device.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __repr__(self) -> str:
4141
return (
4242
f"<{self.__class__.__name__}("
4343
f"id={self.id}, "
44+
f"token={self.token}, "
4445
f"device={self.device}, "
4546
f")>"
4647
)

fastapi_2fa/schemas/token_schema.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
class TokenSchema(BaseModel):
55
access_token: str
6-
refresh_token: str | None
6+
refresh_token: str
77

88

99
class TokenPayload(BaseModel):
1010
sub: int = None
1111
exp: int = None
1212

1313

14-
class PreTfaTokenSchema(BaseModel):
15-
pre_tfa_token: str
14+
class PreTfaTokenSchema(TokenSchema):
15+
refresh_token: None

0 commit comments

Comments
 (0)