11import uuid
2- from datetime import datetime , timedelta
2+ from datetime import datetime , timedelta , timezone
33from typing import Any , Dict , Optional , Union
44
5+ from fastapi import HTTPException
56from jose import ExpiredSignatureError , JWTError , jwt
67from passlib .context import CryptContext
78
@@ -22,7 +23,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
2223
2324# helpers
2425def _now () -> datetime :
25- return datetime .utcnow ( )
26+ return datetime .now ( timezone . utc )
2627
2728
2829def _jti () -> str :
@@ -36,31 +37,35 @@ def create_access_token(
3637 expires_delta : timedelta | None = None ,
3738) -> str :
3839 """
39- Create access token.
40+ Create access token (JWT) with proper iat and exp timestamps .
4041 """
4142
4243 if isinstance (subject , dict ):
43- payload_access : Dict [str , Any ] = subject .copy ()
44+ payload : Dict [str , Any ] = subject .copy ()
4445 else :
45- payload_access : Dict [str , Any ] = {"sub" : str (subject )} # type: ignore
46+ payload : Dict [str , Any ] = {"sub" : str (subject )} # type: ignore
4647
47- payload_access .setdefault ("type" , "access" )
48- payload_access .setdefault ("jti" , _jti ())
49- payload_access .setdefault ("iat" , int (_now ().timestamp ()))
48+ payload .setdefault ("type" , "access" )
49+ payload .setdefault ("jti" , _jti ())
5050
51- if extra :
52- payload_access .update (extra )
53-
54- if expires_delta :
55- exp = _now () + expires_delta
56- else :
57- exp = _now () + timedelta (minutes = settings .JWT_ACCESS_TOKEN_EXPIRE_MINUTES or 15 )
58-
59- payload_access ["exp" ] = int (exp .timestamp ())
51+ now_ts = int (_now ().timestamp ())
52+ payload ["iat" ] = now_ts
6053
61- return jwt .encode (
62- payload_access , settings .SECRET_KEY , algorithm = settings .JWT_ALGORITHM
54+ if extra :
55+ payload .update (extra )
56+
57+ exp_ts = int (
58+ (
59+ _now ()
60+ + (
61+ expires_delta
62+ or timedelta (minutes = settings .PASSWORD_RESET_TOKEN_EXPIRE_MINUTES )
63+ )
64+ ).timestamp ()
6365 )
66+ payload ["exp" ] = exp_ts
67+
68+ return jwt .encode (payload , settings .SECRET_KEY , algorithm = settings .JWT_ALGORITHM )
6469
6570
6671def create_refresh_token (
@@ -69,41 +74,46 @@ def create_refresh_token(
6974 expires_delta : timedelta | None = None ,
7075) -> str :
7176 """
72- Create refresh token.
77+ Create refresh token (JWT) with proper iat and exp timestamps .
7378 """
7479
7580 if isinstance (subject , dict ):
76- payload_refresh : Dict [str , Any ] = subject .copy ()
81+ payload : Dict [str , Any ] = subject .copy ()
7782 else :
78- payload_refresh : Dict [str , Any ] = {"sub" : str (subject )} # type: ignore
83+ payload : Dict [str , Any ] = {"sub" : str (subject )} # type: ignore
7984
80- payload_refresh .setdefault ("type" , "refresh" )
81- payload_refresh .setdefault ("jti" , _jti ())
82- payload_refresh . setdefault ( "iat" , int (_now ().timestamp () ))
85+ payload .setdefault ("type" , "refresh" )
86+ payload .setdefault ("jti" , _jti ())
87+ payload [ "iat" ] = int (_now ().timestamp ())
8388
8489 if extra :
85- payload_refresh .update (extra )
90+ payload .update (extra )
8691
87- if expires_delta :
88- exp = _now () + expires_delta
89- else :
90- exp = _now () + timedelta (days = settings .JWT_REFRESH_TOKEN_EXPIRES_DAYS or 30 )
91-
92- payload_refresh ["exp" ] = int (exp .timestamp ())
93-
94- return jwt .encode (
95- payload_refresh , settings .SECRET_KEY , algorithm = settings .JWT_ALGORITHM
92+ exp_ts = int (
93+ (
94+ _now ()
95+ + (expires_delta or timedelta (days = settings .JWT_REFRESH_TOKEN_EXPIRES_DAYS ))
96+ ).timestamp ()
9697 )
98+ payload ["exp" ] = exp_ts
99+
100+ return jwt .encode (payload , settings .SECRET_KEY , algorithm = settings .JWT_ALGORITHM )
97101
98102
99103def decode_token (token : str ) -> dict :
104+ """
105+ Decode JWT token. By default, disables exp verification for internal inspection.
106+ Use jose.decode(token, ..., options={"verify_exp": True}) when verifying token lifetime.
107+ """
100108 try :
101109 payload = jwt .decode (
102- token , settings .SECRET_KEY , algorithms = [settings .JWT_ALGORITHM ]
110+ token ,
111+ settings .SECRET_KEY ,
112+ algorithms = [settings .JWT_ALGORITHM ],
113+ options = {"verify_exp" : True },
103114 )
104115 return payload
105116 except ExpiredSignatureError :
106- raise ExpiredSignatureError ("Token has expired" )
107-
117+ raise HTTPException (status_code = 400 , detail = "Token has expired" )
108118 except JWTError :
109- raise JWTError ( "Invalid token" )
119+ raise HTTPException ( status_code = 400 , detail = "Invalid token" )
0 commit comments