mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-31 07:38:52 +01:00
feat: Refresh Token for improved Session Security (#927)
* feat(api): refresh token logic * feat(client): refresh token logic * feat(data-provider): refresh token logic * fix: SSE uses esm * chore: add default refresh token expiry to AuthService, add message about env var not set when generating a token * chore: update scripts to more compatible bun methods, ran bun install again * chore: update env.example and playwright workflow with JWT_REFRESH_SECRET * chore: update breaking changes docs * chore: add timeout to url visit * chore: add default SESSION_EXPIRY in generateToken logic, add act script for testing github actions * fix(e2e): refresh automatically in development environment to pass e2e tests
This commit is contained in:
parent
75be9a3279
commit
33f087d38f
31 changed files with 420 additions and 232 deletions
|
|
@ -18,6 +18,15 @@ const setup = ({
|
|||
data: {},
|
||||
isSuccess: false,
|
||||
},
|
||||
useRefreshTokenMutationReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
data: {
|
||||
token: 'mock-token',
|
||||
user: {},
|
||||
},
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -47,12 +56,17 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useGetStartupConfig')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetStartupCongfigReturnValue);
|
||||
const mockUseRefreshTokenMutation = jest
|
||||
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useRefreshTokenMutationReturnValue);
|
||||
const renderResult = render(<Login />);
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseLoginUser,
|
||||
mockUseGetUserQuery,
|
||||
mockUseGetStartupConfig,
|
||||
mockUseRefreshTokenMutation,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ const setup = ({
|
|||
isSuccess: false,
|
||||
error: null as Error | null,
|
||||
},
|
||||
useRefreshTokenMutationReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
data: {
|
||||
token: 'mock-token',
|
||||
user: {},
|
||||
},
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -48,7 +57,10 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useGetStartupConfig')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetStartupCongfigReturnValue);
|
||||
|
||||
const mockUseRefreshTokenMutation = jest
|
||||
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useRefreshTokenMutationReturnValue);
|
||||
const renderResult = render(<Registration />);
|
||||
|
||||
return {
|
||||
|
|
@ -56,6 +68,7 @@ const setup = ({
|
|||
mockUseRegisterUserMutation,
|
||||
mockUseGetUserQuery,
|
||||
mockUseGetStartupConfig,
|
||||
mockUseRefreshTokenMutation,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ const Logout = forwardRef(() => {
|
|||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -113,6 +113,15 @@ const setup = ({
|
|||
plugins: ['wolfram'],
|
||||
},
|
||||
},
|
||||
useRefreshTokenMutationReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
data: {
|
||||
token: 'mock-token',
|
||||
user: {},
|
||||
},
|
||||
},
|
||||
useAvailablePluginsQueryReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
|
|
@ -137,6 +146,10 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useGetUserQuery')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetUserQueryReturnValue);
|
||||
const mockUseRefreshTokenMutation = jest
|
||||
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useRefreshTokenMutationReturnValue);
|
||||
const mockSetIsOpen = jest.fn();
|
||||
const renderResult = render(<PluginStoreDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
|
||||
|
||||
|
|
@ -145,6 +158,7 @@ const setup = ({
|
|||
mockUseGetUserQuery,
|
||||
mockUseAvailablePluginsQuery,
|
||||
mockUseUpdateUserPluginsMutation,
|
||||
mockUseRefreshTokenMutation,
|
||||
mockSetIsOpen,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -107,12 +107,7 @@ const AuthContextProvider = ({
|
|||
});
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
document.cookie.split(';').forEach((c) => {
|
||||
document.cookie = c
|
||||
.replace(/^ +/, '')
|
||||
.replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
|
||||
});
|
||||
const logout = useCallback(() => {
|
||||
logoutUser.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
setUserContext({
|
||||
|
|
@ -126,7 +121,25 @@ const AuthContextProvider = ({
|
|||
doSetError((error as Error).message);
|
||||
},
|
||||
});
|
||||
};
|
||||
}, [setUserContext, logoutUser]);
|
||||
|
||||
const silentRefresh = useCallback(() => {
|
||||
refreshToken.mutate(undefined, {
|
||||
onSuccess: (data: TLoginResponse) => {
|
||||
const { user, token } = data;
|
||||
if (token) {
|
||||
setUserContext({ token, isAuthenticated: true, user });
|
||||
} else {
|
||||
console.log('Token is not present. User is not authenticated.');
|
||||
navigate('/login');
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('refreshToken mutation error:', error);
|
||||
navigate('/login');
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userQuery.data) {
|
||||
|
|
@ -139,12 +152,7 @@ const AuthContextProvider = ({
|
|||
doSetError(undefined);
|
||||
}
|
||||
if (!token || !isAuthenticated) {
|
||||
const tokenFromCookie = getCookieValue('token');
|
||||
if (tokenFromCookie) {
|
||||
setUserContext({ token: tokenFromCookie, isAuthenticated: true, user: userQuery.data });
|
||||
} else {
|
||||
navigate('/login', { replace: true });
|
||||
}
|
||||
silentRefresh();
|
||||
}
|
||||
}, [
|
||||
token,
|
||||
|
|
@ -157,23 +165,23 @@ const AuthContextProvider = ({
|
|||
setUserContext,
|
||||
]);
|
||||
|
||||
// const silentRefresh = useCallback(() => {
|
||||
// refreshToken.mutate(undefined, {
|
||||
// onSuccess: (data: TLoginResponse) => {
|
||||
// const { user, token } = data;
|
||||
// setUserContext({ token, isAuthenticated: true, user });
|
||||
// },
|
||||
// onError: error => {
|
||||
// setError(error.message);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// }, [setUserContext]);
|
||||
useEffect(() => {
|
||||
const handleTokenUpdate = (event) => {
|
||||
console.log('tokenUpdated event received event');
|
||||
const newToken = event.detail;
|
||||
setUserContext({
|
||||
token: newToken,
|
||||
isAuthenticated: true,
|
||||
user: user,
|
||||
});
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// if (token)
|
||||
// silentRefresh();
|
||||
// }, [token, silentRefresh]);
|
||||
window.addEventListener('tokenUpdated', handleTokenUpdate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('tokenUpdated', handleTokenUpdate);
|
||||
};
|
||||
}, [setUserContext, user]);
|
||||
|
||||
// Make the provider update only when it should
|
||||
const memoedValue = useMemo(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue