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:
Danny Avila 2023-09-11 13:10:46 -04:00 committed by GitHub
parent 75be9a3279
commit 33f087d38f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 420 additions and 232 deletions

View file

@ -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,
};
};

View file

@ -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,
};
};

View file

@ -9,7 +9,6 @@ const Logout = forwardRef(() => {
const handleLogout = () => {
logout();
window.location.reload();
};
return (

View file

@ -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,
};
};

View file

@ -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(