Host-remote refresh token implementation (#18044)

This commit is contained in:
nitin
2026-02-25 00:07:29 +05:30
committed by GitHub
parent b56f85f36a
commit 887371054a
22 changed files with 1234 additions and 59 deletions
@@ -9,6 +9,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
@@ -26,6 +27,9 @@ export const useFrontComponentExecutionContext = ({
} => {
const currentUser = useRecoilValueV2(currentUserState);
const navigateApp = useNavigateApp();
const { requestAccessTokenRefresh } = useRequestApplicationTokenRefresh({
frontComponentId,
});
const { navigateCommandMenu } = useNavigateCommandMenu();
const setCommandMenuSearchState = useSetRecoilStateV2(commandMenuSearchState);
const { getIcon } = useIcons();
@@ -115,6 +119,7 @@ export const useFrontComponentExecutionContext = ({
const frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi =
{
navigate,
requestAccessTokenRefresh,
openSidePanelPage,
enqueueSnackbar,
unmountFrontComponent,
@@ -0,0 +1,138 @@
import { ApolloError, useApolloClient } from '@apollo/client';
import { type GraphQLFormattedError } from 'graphql';
import { useRecoilCallback } from 'recoil';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { frontComponentApplicationTokenPairComponentState } from '@/front-components/states/frontComponentApplicationTokenPairComponentState';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import {
FindOneFrontComponentDocument,
type FindOneFrontComponentQuery,
type FindOneFrontComponentQueryVariables,
RenewApplicationTokenDocument,
type RenewApplicationTokenMutation,
type RenewApplicationTokenMutationVariables,
} from '~/generated-metadata/graphql';
const APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED_SUB_CODE =
'APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED';
const hasApplicationRefreshTokenInvalidOrExpiredSubCode = (
errors: ReadonlyArray<GraphQLFormattedError>,
): boolean =>
errors.some(
(error) =>
error.extensions?.subCode ===
APPLICATION_REFRESH_TOKEN_INVALID_OR_EXPIRED_SUB_CODE,
);
type UseRequestApplicationTokenRefreshArgs = {
frontComponentId: string;
};
export const useRequestApplicationTokenRefresh = ({
frontComponentId,
}: UseRequestApplicationTokenRefreshArgs) => {
const apolloClient = useApolloClient();
const applicationTokenPairCallbackState = useRecoilComponentCallbackState(
frontComponentApplicationTokenPairComponentState,
frontComponentId,
);
const requestAccessTokenRefresh = useRecoilCallback(
({ set, snapshot }) =>
async (): Promise<string> => {
const refetchFrontComponentForNewTokenPair =
async (): Promise<string> => {
const result = await apolloClient.query<
FindOneFrontComponentQuery,
FindOneFrontComponentQueryVariables
>({
query: FindOneFrontComponentDocument,
variables: { id: frontComponentId },
fetchPolicy: 'network-only',
});
const newTokenPair =
result.data?.frontComponent?.applicationTokenPair;
if (!isDefined(newTokenPair)) {
throw new Error('Failed to refetch application token pair');
}
set(applicationTokenPairCallbackState, newTokenPair);
return newTokenPair.applicationAccessToken.token;
};
const applicationTokenPair = getSnapshotValue(
snapshot,
applicationTokenPairCallbackState,
);
if (!isDefined(applicationTokenPair)) {
throw new Error(
'Application token pair must be initialized before requesting a refresh. Ensure the front component has loaded its token pair before invoking refresh.',
);
}
// First try renewing via the refresh token (fast path).
// If the refresh token itself is expired, fall back to refetching
// the front component which issues a fresh token pair server-side.
try {
const renewResult = await apolloClient.mutate<
RenewApplicationTokenMutation,
RenewApplicationTokenMutationVariables
>({
mutation: RenewApplicationTokenDocument,
variables: {
applicationRefreshToken:
applicationTokenPair.applicationRefreshToken.token,
},
});
if (isNonEmptyArray(renewResult.errors)) {
if (
hasApplicationRefreshTokenInvalidOrExpiredSubCode(
renewResult.errors,
)
) {
return await refetchFrontComponentForNewTokenPair();
}
const errorMessage = renewResult.errors
.map((error) => error.message)
.join(', ');
throw new Error(`Token renewal failed: ${errorMessage}`);
}
const renewedTokenPair = renewResult.data?.renewApplicationToken;
if (!isDefined(renewedTokenPair)) {
throw new Error('Failed to renew application token');
}
set(applicationTokenPairCallbackState, renewedTokenPair);
return renewedTokenPair.applicationAccessToken.token;
} catch (error) {
if (
error instanceof ApolloError &&
hasApplicationRefreshTokenInvalidOrExpiredSubCode(
error.graphQLErrors,
)
) {
return await refetchFrontComponentForNewTokenPair();
}
throw error;
}
},
[apolloClient, applicationTokenPairCallbackState, frontComponentId],
);
return { requestAccessTokenRefresh };
};