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

@@ -39,6 +39,14 @@ async def get_current_user(request: Request, token: str = Depends(oauth2_scheme)
payload = decode_access_token(token) payload = decode_access_token(token)
if payload is None: if payload is None:
raise credentials_exception 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") user_id: str | None = payload.get("sub")
if user_id is None: if user_id is None:

View File

@@ -1,3 +1,4 @@
import uuid
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional, Any, List from typing import Optional, Any, List
from jose import JWTError, jwt from jose import JWTError, jwt
@@ -56,7 +57,9 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -
to_encode.update({ to_encode.update({
"exp": expire, "exp": expire,
"iat": now, "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) encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt return encoded_jwt
@@ -84,7 +87,8 @@ def create_refresh_token(data: dict, expires_delta: Optional[timedelta] = None)
"exp": expire, "exp": expire,
"iat": now, "iat": now,
"iss": "ea-chatbot-api", "iss": "ea-chatbot-api",
"type": "refresh" "type": "refresh",
"jti": str(uuid.uuid4())
}) })
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm) encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt 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. * It proactively refreshes the session to prevent expiration while the user is active.
*/ */
export function useSilentRefresh(isAuthenticated: boolean) { export function useSilentRefresh(isAuthenticated: boolean) {
const timerRef = useRef<NodeJS.Timeout | null>(null) const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
try { try {

View File

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

View File

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