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:
Aditya Pimpalkar
2024-04-24 10:45:16 +01:00
committed by GitHub
parent 0a7f82333b
commit c63ee519ea
33 changed files with 18564 additions and 15049 deletions

View File

@ -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';

View File

@ -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'
* },
* });
*/

View File

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

View File

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