Skip to content

Commit e88e22e

Browse files
committed
feat: implement token expiration handling and refresh mechanism
- Added functions to check if the access token is expired and to refresh the token using the refresh token. - Updated dependencies to utilize the new token management functions in the authentication flow. - Enhanced the login endpoint to support session management with token state.
1 parent d83a409 commit e88e22e

File tree

3 files changed

+96
-2
lines changed

3 files changed

+96
-2
lines changed

src/backend/config.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import os
22
import json
3+
import time
4+
import httpx
35
import redis
4-
from typing import Optional, Dict, Any
6+
import jwt
7+
from typing import Optional, Dict, Any, Tuple
58
from dotenv import load_dotenv
69

710
load_dotenv()
@@ -60,3 +63,72 @@ def get_auth_url() -> str:
6063
def get_token_url() -> str:
6164
"""Get the token endpoint URL"""
6265
return f"{OIDC_CONFIG['server_url']}/realms/{OIDC_CONFIG['realm']}/protocol/openid-connect/token"
66+
67+
def is_token_expired(token_data: Dict[str, Any], buffer_seconds: int = 30) -> bool:
68+
"""
69+
Check if the access token is expired or about to expire
70+
71+
Args:
72+
token_data: The token data containing the access token
73+
buffer_seconds: Buffer time in seconds to refresh token before it actually expires
74+
75+
Returns:
76+
bool: True if token is expired or about to expire, False otherwise
77+
"""
78+
if not token_data or 'access_token' not in token_data:
79+
return True
80+
81+
try:
82+
# Decode the JWT token without verification to get expiration time
83+
decoded = jwt.decode(token_data['access_token'], options={"verify_signature": False})
84+
85+
# Get expiration time from token
86+
exp_time = decoded.get('exp', 0)
87+
88+
# Check if token is expired or about to expire (with buffer)
89+
current_time = time.time()
90+
return current_time + buffer_seconds >= exp_time
91+
except Exception as e:
92+
print(f"Error checking token expiration: {str(e)}")
93+
return True
94+
95+
async def refresh_token(session_id: str, token_data: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]:
96+
"""
97+
Refresh the access token using the refresh token
98+
99+
Args:
100+
session_id: The session ID
101+
token_data: The current token data containing the refresh token
102+
103+
Returns:
104+
Tuple[bool, Dict[str, Any]]: Success status and updated token data
105+
"""
106+
if not token_data or 'refresh_token' not in token_data:
107+
return False, token_data
108+
109+
try:
110+
async with httpx.AsyncClient() as client:
111+
refresh_response = await client.post(
112+
get_token_url(),
113+
data={
114+
'grant_type': 'refresh_token',
115+
'client_id': OIDC_CONFIG['client_id'],
116+
'client_secret': OIDC_CONFIG['client_secret'],
117+
'refresh_token': token_data['refresh_token']
118+
}
119+
)
120+
121+
if refresh_response.status_code != 200:
122+
print(f"Token refresh failed: {refresh_response.text}")
123+
return False, token_data
124+
125+
# Get new token data
126+
new_token_data = refresh_response.json()
127+
128+
# Update session with new tokens
129+
set_session(session_id, new_token_data)
130+
131+
return True, new_token_data
132+
except Exception as e:
133+
print(f"Error refreshing token: {str(e)}")
134+
return False, token_data

src/backend/dependencies.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Optional
22
from fastapi import Request, HTTPException, Depends
33

4-
from config import get_session
4+
from config import get_session, is_token_expired, refresh_token
55

66
class SessionData:
77
def __init__(self, access_token: str, token_data: dict):
@@ -33,6 +33,23 @@ async def __call__(self, request: Request) -> Optional[SessionData]:
3333
headers={"WWW-Authenticate": "Bearer"},
3434
)
3535
return None
36+
37+
# Check if token is expired and refresh if needed
38+
if is_token_expired(session):
39+
# Try to refresh the token
40+
success, new_session = await refresh_token(session_id, session)
41+
if not success:
42+
# Token refresh failed, user needs to re-authenticate
43+
if self.auto_error:
44+
raise HTTPException(
45+
status_code=401,
46+
detail="Session expired",
47+
headers={"WWW-Authenticate": "Bearer"},
48+
)
49+
return None
50+
# Use the refreshed token data
51+
session = new_session
52+
3653
return SessionData(
3754
access_token=session.get('access_token'),
3855
token_data=session

src/backend/routers/auth.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414

1515
@auth_router.get("/login")
1616
async def login(request: Request, kc_idp_hint: str = None, popup: str = None):
17+
1718
session_id = secrets.token_urlsafe(32)
19+
1820
auth_url = get_auth_url()
1921
state = "popup" if popup == "1" else "default"
22+
2023
if kc_idp_hint:
2124
auth_url = f"{auth_url}&kc_idp_hint={kc_idp_hint}"
25+
2226
# Add state param to OIDC URL
2327
auth_url = f"{auth_url}&state={state}"
28+
2429
response = RedirectResponse(auth_url)
2530
response.set_cookie('session_id', session_id)
2631

0 commit comments

Comments
 (0)