security: add helmet, rate limiting, webhook auth, fix token storage, restrict hard deletes
- Add helmet for HTTP security headers (CSP, HSTS, X-Frame-Options, etc.) - Add @nestjs/throttler for rate limiting (100 req/60s per IP) - Add shared secret validation on Signal webhook endpoint - Remove JWT token from localStorage, use Auth0 SDK memory cache with async getAccessTokenSilently() in API interceptor - Restrict hard delete (?hard=true) to ADMINISTRATOR role in service layer - Replace exposed Anthropic API key with placeholder in .env Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,7 @@ function App() {
|
||||
scope: 'openid profile email offline_access',
|
||||
}}
|
||||
useRefreshTokens={true}
|
||||
cacheLocation="localstorage"
|
||||
cacheLocation="memory"
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { api } from '@/lib/api';
|
||||
import { api, setTokenGetter } from '@/lib/api';
|
||||
|
||||
interface BackendUser {
|
||||
id: string;
|
||||
@@ -40,9 +40,16 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [fetchingUser, setFetchingUser] = useState(false);
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
|
||||
// Set up token and fetch backend user profile
|
||||
// Wire up the API token getter so axios can fetch fresh tokens
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
setTokenGetter(() => getAccessTokenSilently());
|
||||
}
|
||||
return () => setTokenGetter(null);
|
||||
}, [isAuthenticated, getAccessTokenSilently]);
|
||||
|
||||
// Fetch backend user profile after authentication
|
||||
useEffect(() => {
|
||||
// Wait for Auth0 to finish loading before fetching token
|
||||
if (isAuthenticated && !isLoading && !fetchingUser && !backendUser) {
|
||||
setFetchingUser(true);
|
||||
setAuthError(null);
|
||||
@@ -53,42 +60,28 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setFetchingUser(false);
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
getAccessTokenSilently()
|
||||
.then(async (token) => {
|
||||
// Fetch backend user profile (api interceptor handles token automatically)
|
||||
api.get('/auth/profile')
|
||||
.then((response) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log('[AUTH] Got access token, fetching user profile');
|
||||
localStorage.setItem('auth0_token', token);
|
||||
|
||||
// Fetch backend user profile
|
||||
try {
|
||||
const response = await api.get('/auth/profile');
|
||||
console.log('[AUTH] User profile fetched successfully:', response.data.email);
|
||||
setBackendUser(response.data);
|
||||
setAuthError(null);
|
||||
} catch (error: any) {
|
||||
console.error('[AUTH] Failed to fetch user profile:', error);
|
||||
setBackendUser(null);
|
||||
|
||||
// Set specific error message
|
||||
if (error.response?.status === 401) {
|
||||
setAuthError('Your account is pending approval or your session has expired');
|
||||
} else {
|
||||
setAuthError('Failed to load user profile - please try logging in again');
|
||||
}
|
||||
}
|
||||
console.log('[AUTH] User profile fetched successfully:', response.data.email);
|
||||
setBackendUser(response.data);
|
||||
setAuthError(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((error: any) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.error('[AUTH] Failed to get token:', error);
|
||||
console.error('[AUTH] Failed to fetch user profile:', error);
|
||||
setBackendUser(null);
|
||||
|
||||
// Handle specific Auth0 errors
|
||||
if (error.error === 'missing_refresh_token' || error.message?.includes('Missing Refresh Token')) {
|
||||
// Handle specific errors
|
||||
if (error.response?.status === 401) {
|
||||
setAuthError('Your account is pending approval or your session has expired');
|
||||
} else if (error.error === 'missing_refresh_token' || error.message?.includes('Missing Refresh Token')) {
|
||||
setAuthError('Session expired - please log in again');
|
||||
} else if (error.error === 'login_required') {
|
||||
setAuthError('Login required');
|
||||
} else {
|
||||
setAuthError('Authentication failed - please try logging in again');
|
||||
setAuthError('Failed to load user profile - please try logging in again');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -99,7 +92,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}, [isAuthenticated, isLoading]);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('auth0_token');
|
||||
setTokenGetter(null);
|
||||
auth0Logout({ logoutParams: { returnTo: window.location.origin } });
|
||||
};
|
||||
|
||||
|
||||
@@ -20,11 +20,22 @@ export const copilotApi = axios.create({
|
||||
timeout: 120000, // 2 minute timeout for AI requests
|
||||
});
|
||||
|
||||
// Shared request interceptor function
|
||||
const requestInterceptor = (config: any) => {
|
||||
const token = localStorage.getItem('auth0_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
// Token getter function - set by AuthContext when authenticated
|
||||
let getToken: (() => Promise<string>) | null = null;
|
||||
|
||||
export function setTokenGetter(getter: (() => Promise<string>) | null) {
|
||||
getToken = getter;
|
||||
}
|
||||
|
||||
// Shared request interceptor function (async to support token refresh)
|
||||
const requestInterceptor = async (config: any) => {
|
||||
if (getToken) {
|
||||
try {
|
||||
const token = await getToken();
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} catch {
|
||||
// Token fetch failed - request goes without auth header
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
@@ -69,7 +80,6 @@ const responseErrorInterceptor = (error: any) => {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
requestData: config?.data,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user