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

@ -1,4 +1,7 @@
import Crypto from 'crypto-js';
import { openOptionsPage } from '~/background/utils/openOptionsPage';
import { exchangeAuthorizationCode } from '~/db/auth.db';
import { isDefined } from '~/utils/isDefined';
// Open options page programmatically in a new tab.
@ -27,6 +30,11 @@ chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
case 'openOptionsPage':
openOptionsPage();
break;
case 'CONNECT':
launchOAuth(({ status, message }) => {
sendResponse({ status, message });
});
break;
default:
break;
}
@ -34,6 +42,81 @@ 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 }, (tabs) => {
if (isDefined(tabs) && isDefined(tabs[0])) {
chrome.tabs.sendMessage(tabs[0].id ?? 0, {
action: 'AUTHENTICATED',
});
}
});
}
});
}
})
.catch((error) => {
callback({ status: false, message: error.message });
});
};
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
const isDesiredRoute =
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||