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:
@ -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+)?/) ||
|
||||
|
||||
Reference in New Issue
Block a user