Skip to content

Commit

Permalink
chore: remove OAuth from chrome extension (twentyhq#5528)
Browse files Browse the repository at this point in the history
Since we can access the tokens directly from cookies of our front app,
we don't require the OAuth process to fetch tokens anymore
  • Loading branch information
AdityaPimpalkar committed May 23, 2024
1 parent fede721 commit f9a3d5f
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 197 deletions.
118 changes: 31 additions & 87 deletions packages/twenty-chrome-extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import Crypto from 'crypto-js';

import { exchangeAuthorizationCode } from '~/db/auth.db';
import { isDefined } from '~/utils/isDefined';

// Open options page programmatically in a new tab.
Expand All @@ -25,12 +22,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
});
break;
}
case 'launchOAuth': {
launchOAuth(({ status, message }) => {
sendResponse({ status, message });
});
break;
}
case 'openSidepanel': {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
Expand All @@ -57,84 +48,6 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
return true;
});

const generateRandomString = (length: number) => {
const charset =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
let result = '';
for (let i = 0; i < length; i++) {
result += charset.charAt(Math.floor(Math.random() * charset.length));
}
return result;
};

const generateCodeVerifierAndChallenge = () => {
const codeVerifier = generateRandomString(32);
const hash = Crypto.SHA256(codeVerifier);
const codeChallenge = hash
.toString(Crypto.enc.Base64)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');

return { codeVerifier, codeChallenge };
};

const launchOAuth = (
callback: ({ status, message }: { status: boolean; message: string }) => void,
) => {
const { codeVerifier, codeChallenge } = generateCodeVerifierAndChallenge();
const redirectUrl = chrome.identity.getRedirectURL();
chrome.identity
.launchWebAuthFlow({
url: `${
import.meta.env.VITE_FRONT_BASE_URL
}/authorize?clientId=chrome&codeChallenge=${codeChallenge}&redirectUrl=${redirectUrl}`,
interactive: true,
})
.then((responseUrl) => {
if (typeof responseUrl === 'string') {
const url = new URL(responseUrl);
const authorizationCode = url.searchParams.get(
'authorizationCode',
) as string;
exchangeAuthorizationCode({
authorizationCode,
codeVerifier,
}).then((tokens) => {
if (isDefined(tokens)) {
chrome.storage.local.set({
loginToken: tokens.loginToken,
});

chrome.storage.local.set({
accessToken: tokens.accessToken,
});

chrome.storage.local.set({
refreshToken: tokens.refreshToken,
});

callback({ status: true, message: '' });

chrome.tabs.query(
{ active: true, currentWindow: true },
([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.tabs.sendMessage(tab.id, {
action: 'executeContentScript',
});
}
},
);
}
});
}
})
.catch((error) => {
callback({ status: false, message: error.message });
});
};

chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
const isDesiredRoute =
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
Expand All @@ -154,3 +67,34 @@ chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
enabled: true,
});
});

const setTokenStateFromCookie = (cookie: string) => {
const decodedValue = decodeURIComponent(cookie);
const tokenPair = JSON.parse(decodedValue);
if (isDefined(tokenPair)) {
chrome.storage.local.set({
isAuthenticated: true,
accessToken: tokenPair.accessToken,
refreshToken: tokenPair.refreshToken,
});
}
};

chrome.cookies.onChanged.addListener(async ({ cookie }) => {
if (cookie.name === 'tokenPair') {
setTokenStateFromCookie(cookie.value);
}
});

// This will only run the very first time the extension loads, after we have stored the
// cookiesRead variable to true, this will not allow to change the token state everytime background script runs
chrome.cookies.get(
{ name: 'tokenPair', url: `${import.meta.env.VITE_FRONT_BASE_URL}` },
async (cookie) => {
const store = await chrome.storage.local.get(['cookiesRead']);
if (isDefined(cookie) && !isDefined(store.cookiesRead)) {
setTokenStateFromCookie(cookie.value);
chrome.storage.local.set({ cookiesRead: true });
}
},
);
10 changes: 10 additions & 0 deletions packages/twenty-chrome-extension/src/contentScript/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
import { isDefined } from '~/utils/isDefined';

// Inject buttons into the DOM when SPA is reloaded on the resource url.
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
Expand All @@ -21,3 +22,12 @@ chrome.runtime.onMessage.addListener(async (message, _, sendResponse) => {

sendResponse('Executing!');
});

chrome.storage.local.onChanged.addListener(async (store) => {
if (isDefined(store.accessToken)) {
if (isDefined(store.accessToken.newValue)) {
await insertButtonForCompany();
await insertButtonForPerson();
}
}
});
11 changes: 5 additions & 6 deletions packages/twenty-chrome-extension/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { defineManifest } from '@crxjs/vite-plugin';

import packageData from '../package.json';

const host_permissions =
process.env.VITE_MODE === 'development'
? ['https://www.linkedin.com/*', 'http://localhost:3001/*']
: ['https://www.linkedin.com/*'];
const external_sites =
process.env.VITE_MODE === 'development'
? [`https://app.twenty.com/*`, `http://localhost:3001/*`]
Expand Down Expand Up @@ -48,9 +44,12 @@ export default defineManifest({
},
],

permissions: ['activeTab', 'storage', 'identity', 'sidePanel'],
permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'],

host_permissions: host_permissions,
// setting host permissions to all http connections will allow
// for people who host on their custom domain to get access to
// extension instead of white listing individual urls
host_permissions: ['https://*/*', 'http://*/*'],

externally_connectable: {
matches: external_sites,
Expand Down
91 changes: 91 additions & 0 deletions packages/twenty-chrome-extension/src/options/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';

import { TextInput } from '@/ui/input/components/TextInput';
import { isDefined } from '~/utils/isDefined';

const StyledWrapper = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.primary};
display: flex;
height: 100vh;
justify-content: center;
`;

const StyledContainer = styled.div`
width: 400px;
height: 350px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: ${({ theme }) => theme.spacing(8)};
`;

const StyledActionContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
width: 300px;
`;

const Settings = () => {
const [serverBaseUrl, setServerBaseUrl] = useState('');
const [clientUrl, setClientUrl] = useState('');

useEffect(() => {
const getState = async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.serverBaseUrl)) {
setServerBaseUrl(store.serverBaseUrl);
} else {
setServerBaseUrl(import.meta.env.VITE_SERVER_BASE_URL);
}

if (isDefined(store.clientUrl)) {
setClientUrl(store.clientUrl);
} else {
setClientUrl(import.meta.env.VITE_FRONT_BASE_URL);
}
};
void getState();
}, []);

const handleBaseUrlChange = (value: string) => {
setServerBaseUrl(value);
chrome.storage.local.set({ serverBaseUrl: value });
};

const handleClientUrlChange = (value: string) => {
setClientUrl(value);
chrome.storage.local.set({ clientUrl: value });
};

return (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
<StyledActionContainer>
<TextInput
label="Client URL"
value={clientUrl}
onChange={handleClientUrlChange}
placeholder="My client URL"
fullWidth
/>
<TextInput
label="Server URL"
value={serverBaseUrl}
onChange={handleBaseUrlChange}
placeholder="My server URL"
fullWidth
/>
</StyledActionContainer>
</StyledContainer>
</StyledWrapper>
);
};

export default Settings;
Loading

0 comments on commit f9a3d5f

Please sign in to comment.