chore: remove OAuth from chrome extension (#5528)

Since we can access the tokens directly from cookies of our front app,
we don't require the OAuth process to fetch tokens anymore
This commit is contained in:
Aditya Pimpalkar
2024-05-23 23:01:47 +01:00
committed by GitHub
parent fede721ba8
commit f9a3d5fd15
5 changed files with 194 additions and 205 deletions

View File

@ -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.
@ -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)) {
@ -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+)?/) ||
@ -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 });
}
},
);

View File

@ -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/
@ -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();
}
}
});

View File

@ -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/*`]
@ -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,

View File

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

View File

@ -1,9 +1,7 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Loader } from '@/ui/display/loader/components/Loader';
import { MainButton } from '@/ui/input/button/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { isDefined } from '~/utils/isDefined';
const StyledIframe = styled.iframe`
@ -41,126 +39,73 @@ const StyledActionContainer = styled.div`
`;
const Sidepanel = () => {
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [iframeSrc, setIframeSrc] = useState(
const [clientUrl, setClientUrl] = useState(
import.meta.env.VITE_FRONT_BASE_URL,
);
const [error, setError] = useState('');
const [serverBaseUrl, setServerBaseUrl] = useState('');
const authenticate = () => {
setIsAuthenticating(true);
setError('');
chrome.runtime.sendMessage(
{ action: 'launchOAuth' },
({ status, message }) => {
if (status === true) {
setIsAuthenticated(true);
setIsAuthenticating(false);
chrome.storage.local.set({ isAuthenticated: true });
} else {
setError(message);
setIsAuthenticating(false);
const iframeRef = useRef<HTMLIFrameElement>(null);
const setIframeState = useCallback(async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.isAuthenticated)) setIsAuthenticated(true);
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
if (
isDefined(activeTab) &&
isDefined(store[`sidepanelUrl_${activeTab.id}`])
) {
const url = store[`sidepanelUrl_${activeTab.id}`];
setClientUrl(url);
} else if (isDefined(store.clientUrl)) {
setClientUrl(store.clientUrl);
}
}, [setClientUrl]);
useEffect(() => {
const initState = async () => {
const store = await chrome.storage.local.get();
if (isDefined(store.isAuthenticated)) setIsAuthenticated(true);
void setIframeState();
};
void initState();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
void setIframeState();
}, [setIframeState, clientUrl]);
useEffect(() => {
chrome.storage.local.onChanged.addListener((store) => {
if (isDefined(store.isAuthenticated)) {
if (store.isAuthenticated.newValue === true) {
setIframeState();
}
},
);
};
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 (store.isAuthenticated === true) setIsAuthenticated(true);
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
if (
isDefined(activeTab) &&
isDefined(store[`sidepanelUrl_${activeTab.id}`])
) {
const url = store[`sidepanelUrl_${activeTab.id}`];
setIframeSrc(url);
}
};
void getState();
}, []);
useEffect(() => {
const handleBrowserEvents = ({ action }: { action: string }) => {
if (action === 'changeSidepanelUrl') {
setIframeSrc('');
}
};
chrome.runtime.onMessage.addListener(handleBrowserEvents);
return () => {
chrome.runtime.onMessage.removeListener(handleBrowserEvents);
};
}, []);
useEffect(() => {
const getIframeState = async () => {
const store = await chrome.storage.local.get();
const { tab: activeTab } = await chrome.runtime.sendMessage({
action: 'getActiveTab',
});
if (
isDefined(activeTab) &&
isDefined(store[`sidepanelUrl_${activeTab.id}`])
) {
const url = store[`sidepanelUrl_${activeTab.id}`];
setIframeSrc(url);
}
};
void getIframeState();
}, [iframeSrc]);
const handleBaseUrlChange = (value: string) => {
setServerBaseUrl(value);
setError('');
chrome.storage.local.set({ serverBaseUrl: value });
};
});
}, [setIframeState]);
return isAuthenticated ? (
<StyledIframe title="twenty-website" src={iframeSrc}></StyledIframe>
<StyledIframe
ref={iframeRef}
title="twenty-website"
src={clientUrl}
></StyledIframe>
) : (
<StyledWrapper>
<StyledContainer>
<img src="/logo/32-32.svg" alt="twenty-logo" height={40} width={40} />
{isAuthenticating ? (
<Loader />
) : (
<StyledActionContainer>
<TextInput
label="Server URL"
value={serverBaseUrl}
onChange={handleBaseUrlChange}
placeholder="My base server URL"
error={error}
fullWidth
/>
<MainButton
title="Connect your account"
onClick={() => authenticate()}
fullWidth
/>
<MainButton
title="Sign up"
variant="secondary"
onClick={() =>
window.open(`${import.meta.env.VITE_FRONT_BASE_URL}`, '_blank')
}
fullWidth
/>
</StyledActionContainer>
)}
<StyledActionContainer>
<MainButton
title="Connect your account"
fullWidth
onClick={() => {
window.open(clientUrl, '_blank');
}}
/>
</StyledActionContainer>
</StyledContainer>
</StyledWrapper>
);