feat: oauth for chrome extension (#4870)
Previously we had to create a separate API key to give access to chrome extension so we can make calls to the DB. This PR includes logic to initiate a oauth flow with PKCE method which redirects to the `Authorise` screen to give access to server tokens. Implemented in this PR- 1. make `redirectUrl` a non-nullable parameter 2. Add `NODE_ENV` to environment variable service 3. new env variable `CHROME_EXTENSION_REDIRECT_URL` on server side 4. strict checks for redirectUrl 5. try catch blocks on utils db query methods 6. refactor Apollo Client to handle `unauthorized` condition 7. input field to enter server url (for self-hosting) 8. state to show user if its already connected 9. show error if oauth flow is cancelled by user Follow up PR - Renew token logic --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -10,7 +10,7 @@ import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
||||
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
||||
import Authorize from '~/pages/auth/Authorize';
|
||||
import { Authorize } from '~/pages/auth/Authorize';
|
||||
import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan.tsx';
|
||||
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
||||
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
||||
|
||||
@ -255,6 +255,7 @@ export type Mutation = {
|
||||
deleteOneObject: Object;
|
||||
deleteUser: User;
|
||||
emailPasswordResetLink: EmailPasswordResetLink;
|
||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||
generateApiKeyToken: ApiKeyToken;
|
||||
generateJWT: AuthTokens;
|
||||
generateTransientToken: TransientToken;
|
||||
@ -282,7 +283,7 @@ export type MutationActivateWorkspaceArgs = {
|
||||
export type MutationAuthorizeAppArgs = {
|
||||
clientId: Scalars['String'];
|
||||
codeChallenge?: InputMaybe<Scalars['String']>;
|
||||
redirectUrl?: InputMaybe<Scalars['String']>;
|
||||
redirectUrl: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
@ -308,6 +309,13 @@ export type MutationEmailPasswordResetLinkArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationExchangeAuthorizationCodeArgs = {
|
||||
authorizationCode: Scalars['String'];
|
||||
clientSecret?: InputMaybe<Scalars['String']>;
|
||||
codeVerifier?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationGenerateApiKeyTokenArgs = {
|
||||
apiKeyId: Scalars['String'];
|
||||
expiresAt: Scalars['String'];
|
||||
@ -429,7 +437,6 @@ export type Query = {
|
||||
clientConfig: ClientConfig;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
exchangeAuthorizationCode: ExchangeAuthCode;
|
||||
findWorkspaceFromInviteHash: Workspace;
|
||||
getProductPrices: ProductPricesEntity;
|
||||
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
|
||||
@ -457,13 +464,6 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryExchangeAuthorizationCodeArgs = {
|
||||
authorizationCode: Scalars['String'];
|
||||
clientSecret?: InputMaybe<Scalars['String']>;
|
||||
codeVerifier?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindWorkspaceFromInviteHashArgs = {
|
||||
inviteHash: Scalars['String'];
|
||||
};
|
||||
@ -988,6 +988,7 @@ export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessT
|
||||
export type AuthorizeAppMutationVariables = Exact<{
|
||||
clientId: Scalars['String'];
|
||||
codeChallenge: Scalars['String'];
|
||||
redirectUrl: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
@ -1517,8 +1518,12 @@ export type TrackMutationHookResult = ReturnType<typeof useTrackMutation>;
|
||||
export type TrackMutationResult = Apollo.MutationResult<TrackMutation>;
|
||||
export type TrackMutationOptions = Apollo.BaseMutationOptions<TrackMutation, TrackMutationVariables>;
|
||||
export const AuthorizeAppDocument = gql`
|
||||
mutation authorizeApp($clientId: String!, $codeChallenge: String!) {
|
||||
authorizeApp(clientId: $clientId, codeChallenge: $codeChallenge) {
|
||||
mutation authorizeApp($clientId: String!, $codeChallenge: String!, $redirectUrl: String!) {
|
||||
authorizeApp(
|
||||
clientId: $clientId
|
||||
codeChallenge: $codeChallenge
|
||||
redirectUrl: $redirectUrl
|
||||
) {
|
||||
redirectUrl
|
||||
}
|
||||
}
|
||||
@ -1540,6 +1545,7 @@ export type AuthorizeAppMutationFn = Apollo.MutationFunction<AuthorizeAppMutatio
|
||||
* variables: {
|
||||
* clientId: // value for 'clientId'
|
||||
* codeChallenge: // value for 'codeChallenge'
|
||||
* redirectUrl: // value for 'redirectUrl'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const AUTHORIZE_APP = gql`
|
||||
mutation authorizeApp($clientId: String!, $codeChallenge: String!) {
|
||||
authorizeApp(clientId: $clientId, codeChallenge: $codeChallenge) {
|
||||
mutation authorizeApp(
|
||||
$clientId: String!
|
||||
$codeChallenge: String!
|
||||
$redirectUrl: String!
|
||||
) {
|
||||
authorizeApp(
|
||||
clientId: $clientId
|
||||
codeChallenge: $codeChallenge
|
||||
redirectUrl: $redirectUrl
|
||||
) {
|
||||
redirectUrl
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ const StyledButtonContainer = styled.div`
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
`;
|
||||
const Authorize = () => {
|
||||
export const Authorize = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParam] = useSearchParams();
|
||||
//TODO: Replace with db call for registered third party apps
|
||||
@ -66,6 +66,7 @@ const Authorize = () => {
|
||||
const [app, setApp] = useState<App>();
|
||||
const clientId = searchParam.get('clientId');
|
||||
const codeChallenge = searchParam.get('codeChallenge');
|
||||
const redirectUrl = searchParam.get('redirectUrl');
|
||||
|
||||
useEffect(() => {
|
||||
const app = apps.find((app) => app.id === clientId);
|
||||
@ -76,18 +77,20 @@ const Authorize = () => {
|
||||
|
||||
const [authorizeApp] = useAuthorizeAppMutation();
|
||||
const handleAuthorize = async () => {
|
||||
if (isDefined(clientId) && isDefined(codeChallenge)) {
|
||||
if (
|
||||
isDefined(clientId) &&
|
||||
isDefined(codeChallenge) &&
|
||||
isDefined(redirectUrl)
|
||||
) {
|
||||
await authorizeApp({
|
||||
variables: {
|
||||
clientId,
|
||||
codeChallenge,
|
||||
redirectUrl,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
window.location.href = data.authorizeApp.redirectUrl;
|
||||
},
|
||||
onError: (error) => {
|
||||
throw Error(error.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -124,5 +127,3 @@ const Authorize = () => {
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Authorize;
|
||||
|
||||
Reference in New Issue
Block a user