fix(auth): Address high and medium priority security and build findings
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user