fix(auth): Address high and medium priority security and build findings

This commit is contained in:
Yunxiao Xu
2026-02-18 14:50:09 -08:00
parent 6131f27142
commit f5aeb9d956
5 changed files with 32 additions and 12 deletions

View File

@@ -40,6 +40,14 @@ async def get_current_user(request: Request, token: str = Depends(oauth2_scheme)
if payload is None:
raise credentials_exception
# Security Fix: Reject refresh tokens for standard API access
if payload.get("type") == "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Cannot use refresh token for this endpoint",
headers={"WWW-Authenticate": "Bearer"},
)
user_id: str | None = payload.get("sub")
if user_id is None:
raise credentials_exception

View File

@@ -1,3 +1,4 @@
import uuid
from datetime import datetime, timedelta, timezone
from typing import Optional, Any, List
from jose import JWTError, jwt
@@ -56,7 +57,9 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
to_encode.update({
"exp": expire,
"iat": now,
"iss": "ea-chatbot-api"
"iss": "ea-chatbot-api",
"type": "access",
"jti": str(uuid.uuid4())
})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt
@@ -84,7 +87,8 @@ def create_refresh_token(data: dict, expires_delta: Optional[timedelta] = None)
"exp": expire,
"iat": now,
"iss": "ea-chatbot-api",
"type": "refresh"
"type": "refresh",
"jti": str(uuid.uuid4())
})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt

View File

@@ -6,7 +6,7 @@ import { AuthService } from '@/services/auth'
* It proactively refreshes the session to prevent expiration while the user is active.
*/
export function useSilentRefresh(isAuthenticated: boolean) {
const timerRef = useRef<NodeJS.Timeout | null>(null)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const refresh = useCallback(async () => {
try {

View File

@@ -4,7 +4,6 @@ import './index.css'
import App from './App.tsx'
import { TooltipProvider } from "@/components/ui/tooltip"
import { BrowserRouter } from "react-router-dom"
import { ThemeProvider } from "./components/theme-provider"
createRoot(document.getElementById('root')!).render(
<StrictMode>

View File

@@ -17,14 +17,23 @@ export const registerUnauthorizedCallback = (callback: () => void) => {
// State to manage multiple concurrent refreshes
let isRefreshing = false
let refreshSubscribers: ((token: string) => void)[] = []
let refreshErrorSubscribers: ((error: any) => void)[] = []
const subscribeTokenRefresh = (callback: (token: string) => void) => {
refreshSubscribers.push(callback)
const subscribeTokenRefresh = (onSuccess: (token: string) => void, onError: (error: any) => void) => {
refreshSubscribers.push(onSuccess)
refreshErrorSubscribers.push(onError)
}
const onRefreshed = (token: string) => {
refreshSubscribers.forEach((callback) => callback(token))
refreshSubscribers = []
refreshErrorSubscribers = []
}
const onRefreshFailed = (error: any) => {
refreshErrorSubscribers.forEach((callback) => callback(error))
refreshSubscribers = []
refreshErrorSubscribers = []
}
// Add a response interceptor to handle 401s
@@ -41,11 +50,11 @@ api.interceptors.response.use(
if (error.response?.status === 401 && !isAuthEndpoint && !isRefreshEndpoint && !originalRequest._retry) {
if (isRefreshing) {
// Wait for the current refresh to complete
return new Promise((resolve) => {
subscribeTokenRefresh((token) => {
// Re-run the original request
resolve(api(originalRequest))
})
return new Promise((resolve, reject) => {
subscribeTokenRefresh(
(_token) => resolve(api(originalRequest)),
(err) => reject(err)
)
})
}
@@ -65,7 +74,7 @@ api.interceptors.response.use(
return api(originalRequest)
} catch (refreshError) {
isRefreshing = false
refreshSubscribers = []
onRefreshFailed(refreshError)
console.error("Reactive refresh failed:", refreshError)
// Final failure - session is dead