MukeshKapoor25 commited on
Commit
4ddb8b3
·
1 Parent(s): fbc46d2

feat(auth): implement refresh token functionality

Browse files

- Add create_refresh_token utility function with 7-day expiration
- Update TokenResponse schema to include refresh_token field
- Modify login and registration endpoints to return refresh tokens
- Add new /refresh-token endpoint for token rotation
- Include user_exists flag in OTPSendResponse for better UX

app/routers/user_router.py CHANGED
@@ -9,12 +9,14 @@ from app.schemas.user_schema import (
9
  UserLoginRequest,
10
  OAuthLoginRequest,
11
  TokenResponse,
 
12
  )
13
  from app.services.user_service import UserService
14
- from app.utils.jwt import create_temp_token, decode_token
15
  from app.utils.social_utils import verify_google_token, verify_apple_token, verify_facebook_token
16
  from app.utils.common_utils import validate_identifier
17
  from app.models.social_security_model import SocialSecurityModel
 
18
  from fastapi import Request
19
  import logging
20
 
@@ -54,55 +56,10 @@ def get_bearer_token(api_key: str = Security(api_key_scheme)) -> str:
54
  status_code=status.HTTP_401_UNAUTHORIZED,
55
  detail="Invalid Authorization header format"
56
  )
57
-
58
- # 📧📱 Send OTP to email or phone (legacy endpoint - supports both fields)
59
- @router.post("/send-otp")
60
- async def send_otp_handler(payload: OTPRequest):
61
- logger.info(f"OTP request started - email: {payload.email}, phone: {payload.phone}")
62
-
63
- try:
64
- # Validate input - ensure only one identifier is provided
65
- identifier = payload.email or payload.phone
66
- if not identifier:
67
- logger.warning("OTP request failed - neither email nor phone provided")
68
- raise HTTPException(status_code=400, detail="Email or phone required")
69
-
70
- # Validate identifier format
71
- try:
72
- identifier_type = validate_identifier(identifier)
73
- logger.debug(f"Identifier type: {identifier_type}")
74
- except ValueError as ve:
75
- logger.error(f"Invalid identifier format: {str(ve)}")
76
- raise HTTPException(status_code=400, detail=str(ve))
77
-
78
- logger.debug(f"Using identifier: {identifier}")
79
-
80
- # Send OTP via service
81
- logger.debug(f"Calling UserService.send_otp with identifier: {identifier}")
82
- await UserService.send_otp(identifier)
83
- logger.info(f"OTP sent successfully to: {identifier}")
84
-
85
- # Create temporary token
86
- logger.debug("Creating temporary token for OTP verification")
87
- temp_token = create_temp_token({
88
- "sub": identifier,
89
- "type": "otp_verification"
90
- }, expires_minutes=10)
91
-
92
- logger.info(f"Temporary token created for: {identifier}")
93
- logger.debug(f"Temp token (first 20 chars): {temp_token[:20]}...")
94
-
95
- return {"message": "OTP sent", "temp_token": temp_token}
96
-
97
- except HTTPException as e:
98
- logger.error(f"OTP request failed - HTTP {e.status_code}: {e.detail}")
99
- raise e
100
- except Exception as e:
101
- logger.error(f"Unexpected error during OTP request: {str(e)}", exc_info=True)
102
- raise HTTPException(status_code=500, detail="Internal server error during OTP request")
103
 
104
  # 📧📱 Send OTP using single login input (preferred endpoint)
105
- @router.post("/send-otp-login")
106
  async def send_otp_login_handler(payload: OTPRequestWithLogin):
107
  logger.info(f"OTP login request started - login_input: {payload.login_input}")
108
 
@@ -115,6 +72,15 @@ async def send_otp_login_handler(payload: OTPRequestWithLogin):
115
  logger.error(f"Invalid login input format: {str(ve)}")
116
  raise HTTPException(status_code=400, detail=str(ve))
117
 
 
 
 
 
 
 
 
 
 
118
  # Send OTP via service
119
  logger.debug(f"Calling UserService.send_otp with identifier: {payload.login_input}")
120
  await UserService.send_otp(payload.login_input)
@@ -130,7 +96,11 @@ async def send_otp_login_handler(payload: OTPRequestWithLogin):
130
  logger.info(f"Temporary token created for: {payload.login_input}")
131
  logger.debug(f"Temp token (first 20 chars): {temp_token[:20]}...")
132
 
133
- return {"message": "OTP sent", "temp_token": temp_token}
 
 
 
 
134
 
135
  except HTTPException as e:
136
  logger.error(f"OTP login request failed - HTTP {e.status_code}: {e.detail}")
@@ -282,4 +252,69 @@ async def register_user(
282
 
283
  logger.info(f"Registering user with payload: {payload}")
284
 
285
- return await UserService.register(payload, decoded)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  UserLoginRequest,
10
  OAuthLoginRequest,
11
  TokenResponse,
12
+ OTPSendResponse,
13
  )
14
  from app.services.user_service import UserService
15
+ from app.utils.jwt import create_temp_token, decode_token, create_refresh_token
16
  from app.utils.social_utils import verify_google_token, verify_apple_token, verify_facebook_token
17
  from app.utils.common_utils import validate_identifier
18
  from app.models.social_security_model import SocialSecurityModel
19
+ from app.models.user_model import BookMyServiceUserModel
20
  from fastapi import Request
21
  import logging
22
 
 
56
  status_code=status.HTTP_401_UNAUTHORIZED,
57
  detail="Invalid Authorization header format"
58
  )
59
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  # 📧📱 Send OTP using single login input (preferred endpoint)
62
+ @router.post("/send-otp-login", response_model=OTPSendResponse)
63
  async def send_otp_login_handler(payload: OTPRequestWithLogin):
64
  logger.info(f"OTP login request started - login_input: {payload.login_input}")
65
 
 
72
  logger.error(f"Invalid login input format: {str(ve)}")
73
  raise HTTPException(status_code=400, detail=str(ve))
74
 
75
+ # Check if user already exists
76
+ user_exists = False
77
+ if identifier_type == "email":
78
+ user_exists = await BookMyServiceUserModel.exists_by_email_or_phone(email=payload.login_input)
79
+ elif identifier_type == "phone":
80
+ user_exists = await BookMyServiceUserModel.exists_by_email_or_phone(phone=payload.login_input)
81
+
82
+ logger.debug(f"User existence check result: {user_exists}")
83
+
84
  # Send OTP via service
85
  logger.debug(f"Calling UserService.send_otp with identifier: {payload.login_input}")
86
  await UserService.send_otp(payload.login_input)
 
96
  logger.info(f"Temporary token created for: {payload.login_input}")
97
  logger.debug(f"Temp token (first 20 chars): {temp_token[:20]}...")
98
 
99
+ return {
100
+ "message": "OTP sent",
101
+ "temp_token": temp_token,
102
+ "user_exists": user_exists
103
+ }
104
 
105
  except HTTPException as e:
106
  logger.error(f"OTP login request failed - HTTP {e.status_code}: {e.detail}")
 
252
 
253
  logger.info(f"Registering user with payload: {payload}")
254
 
255
+ return await UserService.register(payload, decoded)
256
+
257
+ # 🔄 Refresh access token using refresh token
258
+ @router.post("/refresh-token", response_model=TokenResponse)
259
+ async def refresh_token_handler(refresh_token: str = Depends(get_bearer_token)):
260
+ logger.info("Refresh token request received")
261
+
262
+ try:
263
+ # Decode and validate refresh token
264
+ decoded = decode_token(refresh_token)
265
+ logger.debug(f"Decoded refresh token payload: {decoded}")
266
+
267
+ if not decoded:
268
+ logger.warning("Failed to decode refresh token - token is invalid or expired")
269
+ raise HTTPException(status_code=401, detail="Invalid or expired refresh token")
270
+
271
+ # Validate token type
272
+ token_type = decoded.get("type")
273
+ if token_type != "refresh":
274
+ logger.warning(f"Invalid token type for refresh - expected: refresh, got: {token_type}")
275
+ raise HTTPException(status_code=401, detail="Invalid refresh token")
276
+
277
+ # Extract user information from refresh token
278
+ user_id = decoded.get("sub")
279
+ if not user_id:
280
+ logger.warning("Refresh token missing subject (user_id)")
281
+ raise HTTPException(status_code=401, detail="Invalid refresh token")
282
+
283
+ logger.info(f"Refresh token validation successful for user: {user_id}")
284
+
285
+ # Create new access token
286
+ access_token = create_access_token({
287
+ "sub": user_id,
288
+ "user_id": user_id,
289
+ "email": decoded.get("email"),
290
+ "phone": decoded.get("phone"),
291
+ "role": "user"
292
+ }, expires_minutes=60) # 1 hour access token
293
+
294
+ # Optionally create a new refresh token (refresh token rotation)
295
+ new_refresh_token = create_refresh_token({
296
+ "sub": user_id,
297
+ "user_id": user_id,
298
+ "email": decoded.get("email"),
299
+ "phone": decoded.get("phone"),
300
+ "role": "user"
301
+ }, expires_days=7) # 7 days refresh token
302
+
303
+ logger.info(f"New access token generated for user: {user_id}")
304
+
305
+ return {
306
+ "access_token": access_token,
307
+ "token_type": "bearer",
308
+ "expires_in": 3600, # 1 hour in seconds
309
+ "refresh_token": new_refresh_token,
310
+ "user_id": user_id,
311
+ "email": decoded.get("email"),
312
+ "phone": decoded.get("phone")
313
+ }
314
+
315
+ except HTTPException as e:
316
+ logger.error(f"Refresh token failed - HTTP {e.status_code}: {e.detail}")
317
+ raise e
318
+ except Exception as e:
319
+ logger.error(f"Unexpected error during refresh token: {str(e)}", exc_info=True)
320
+ raise HTTPException(status_code=500, detail="Internal server error during token refresh")
app/schemas/user_schema.py CHANGED
@@ -106,6 +106,12 @@ class OTPVerifyRequest(BaseModel):
106
  raise ValueError('Login input must be a valid email address or phone number')
107
  return v
108
 
 
 
 
 
 
 
109
  # OAuth login using Google/Apple
110
  class OAuthLoginRequest(BaseModel):
111
  provider: Literal["google", "apple", "facebook"]
@@ -116,6 +122,7 @@ class TokenResponse(BaseModel):
116
  access_token: str
117
  token_type: str = "bearer"
118
  expires_in: Optional[int] = 28800 # 8 hours in seconds
 
119
  user_id: Optional[str] = None
120
  name: Optional[str] = None
121
  email: Optional[str] = None
 
106
  raise ValueError('Login input must be a valid email address or phone number')
107
  return v
108
 
109
+ # OTP send response with user existence flag
110
+ class OTPSendResponse(BaseModel):
111
+ message: str
112
+ temp_token: str
113
+ user_exists: bool = False
114
+
115
  # OAuth login using Google/Apple
116
  class OAuthLoginRequest(BaseModel):
117
  provider: Literal["google", "apple", "facebook"]
 
122
  access_token: str
123
  token_type: str = "bearer"
124
  expires_in: Optional[int] = 28800 # 8 hours in seconds
125
+ refresh_token: Optional[str] = None
126
  user_id: Optional[str] = None
127
  name: Optional[str] = None
128
  email: Optional[str] = None
app/services/user_service.py CHANGED
@@ -116,9 +116,25 @@ class UserService:
116
  logger.debug(f"Token data: {token_data}")
117
 
118
  access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
119
- logger.info(f"JWT token created successfully for user: {user.get('user_id')}")
120
 
121
- return {"access_token": access_token, "token_type": "bearer", "name": user.get("name")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  except ValueError as ve:
124
  logger.error(f"Validation error for identifier {identifier}: {str(ve)}")
@@ -204,7 +220,22 @@ class UserService:
204
  "exp": datetime.utcnow() + timedelta(hours=8)
205
  }
206
  access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
207
- return {"access_token": access_token}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  user_id = f"{data.provider}_{provider_user_id}"
210
 
@@ -257,8 +288,19 @@ class UserService:
257
  }
258
 
259
  access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
 
 
 
 
 
 
 
 
 
 
260
  return {
261
  "access_token": access_token,
 
262
  "token_type": "bearer",
263
  "expires_in": 28800,
264
  "user_id": user_id,
 
116
  logger.debug(f"Token data: {token_data}")
117
 
118
  access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
 
119
 
120
+ # Create refresh token
121
+ refresh_token_data = {
122
+ "sub": user.get("user_id"),
123
+ "user_id": user.get("user_id"),
124
+ "type": "refresh",
125
+ "exp": datetime.utcnow() + timedelta(days=7)
126
+ }
127
+ refresh_token = jwt.encode(refresh_token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
128
+
129
+ logger.info(f"JWT tokens created successfully for user: {user.get('user_id')}")
130
+
131
+ return {
132
+ "access_token": access_token,
133
+ "refresh_token": refresh_token,
134
+ "token_type": "bearer",
135
+ "expires_in": 28800,
136
+ "name": user.get("name")
137
+ }
138
 
139
  except ValueError as ve:
140
  logger.error(f"Validation error for identifier {identifier}: {str(ve)}")
 
220
  "exp": datetime.utcnow() + timedelta(hours=8)
221
  }
222
  access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
223
+
224
+ # Create refresh token
225
+ refresh_token_data = {
226
+ "sub": existing_user["user_id"],
227
+ "user_id": existing_user["user_id"],
228
+ "type": "refresh",
229
+ "exp": datetime.utcnow() + timedelta(days=7)
230
+ }
231
+ refresh_token = jwt.encode(refresh_token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
232
+
233
+ return {
234
+ "access_token": access_token,
235
+ "refresh_token": refresh_token,
236
+ "token_type": "bearer",
237
+ "expires_in": 28800
238
+ }
239
 
240
  user_id = f"{data.provider}_{provider_user_id}"
241
 
 
288
  }
289
 
290
  access_token = jwt.encode(token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
291
+
292
+ # Create refresh token
293
+ refresh_token_data = {
294
+ "sub": user_id,
295
+ "user_id": user_id,
296
+ "type": "refresh",
297
+ "exp": datetime.utcnow() + timedelta(days=7)
298
+ }
299
+ refresh_token = jwt.encode(refresh_token_data, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
300
+
301
  return {
302
  "access_token": access_token,
303
+ "refresh_token": refresh_token,
304
  "token_type": "bearer",
305
  "expires_in": 28800,
306
  "user_id": user_id,
app/utils/jwt.py CHANGED
@@ -20,6 +20,12 @@ def create_access_token(data: dict, expires_minutes: int = 60):
20
  to_encode.update({"exp": expire})
21
  return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
22
 
 
 
 
 
 
 
23
  def create_temp_token(data: dict, expires_minutes: int = 10):
24
  return create_access_token(data, expires_minutes=expires_minutes)
25
 
 
20
  to_encode.update({"exp": expire})
21
  return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
22
 
23
+ def create_refresh_token(data: dict, expires_days: int = 7):
24
+ to_encode = data.copy()
25
+ expire = datetime.utcnow() + timedelta(days=expires_days)
26
+ to_encode.update({"exp": expire, "type": "refresh"})
27
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
28
+
29
  def create_temp_token(data: dict, expires_minutes: int = 10):
30
  return create_access_token(data, expires_minutes=expires_minutes)
31