diff --git a/src/constants.ts b/src/constants.ts index 14c4944e..b28a3da5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,10 +1,11 @@ // OpenSauced constants export const OPEN_SAUCED_INSIGHTS_DOMAIN = import.meta.env.VITE_OPEN_SAUCED_INSIGHTS_DOMAIN; export const OPEN_SAUCED_API_ENDPOINT = import.meta.env.VITE_OPEN_SAUCED_API_ENDPOINT; -export const SUPABASE_LOGIN_URL = `https://${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}.supabase.co/auth/v1/authorize?provider=github&redirect_to=https://${OPEN_SAUCED_INSIGHTS_DOMAIN}/`; +export const SUPABASE_LOGIN_URL = `https://${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}.supabase.co/auth/v1/authorize`; export const SUPABASE_AUTH_COOKIE_NAME = `sb-${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}-auth-token`; +export const SUPABASE_PKCE_VERIFIER_COOKIE_NAME = `sb-${import.meta.env.VITE_OPEN_SAUCED_SUPABASE_ID}-auth-token-code-verifier`; export const OPEN_SAUCED_AUTH_TOKEN_KEY = "os-access-token"; export const OPEN_SAUCED_OPTED_LOG_OUT_KEY = "opted-log-out"; export const AI_PR_DESCRIPTION_CONFIG_KEY = "ai-pr-description-config"; diff --git a/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts b/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts index c5d76ddf..9c52ba7a 100644 --- a/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts +++ b/src/content-scripts/components/AICodeReview/AICodeReviewMenu.ts @@ -1,13 +1,10 @@ -import { - SUPABASE_LOGIN_URL, - GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR, -} from "../../../constants"; +import { GITHUB_PR_SUGGESTION_TEXT_AREA_SELECTOR } from "../../../constants"; import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert"; import { DescriptionConfig, getAIDescriptionConfig, } from "../../../utils/ai-utils/descriptionconfig"; -import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; +import { getAuthToken, isLoggedIn, optLogIn } from "../../../utils/checkAuthentication"; import { createHtmlElement } from "../../../utils/createHtmlElement"; import { isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; @@ -73,7 +70,7 @@ const handleSubmit = async ( try { if (!(await isLoggedIn())) { - return window.open(SUPABASE_LOGIN_URL, "_blank"); + return void optLogIn(); } if (!logo || !button) { diff --git a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts index f5374fca..e5dd01d5 100644 --- a/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts +++ b/src/content-scripts/components/GenerateAIDescription/DescriptionGeneratorButton.ts @@ -3,10 +3,10 @@ import openSaucedLogoIcon from "../../../assets/opensauced-icon.svg"; import { getPullRequestAPIURL } from "../../../utils/urlMatchers"; import { getDescriptionContext, isOutOfContextBounds } from "../../../utils/fetchGithubAPIData"; import { generateDescription } from "../../../utils/ai-utils/openai"; -import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR, SUPABASE_LOGIN_URL } from "../../../constants"; +import { GITHUB_PR_COMMENT_TEXT_AREA_SELECTOR } from "../../../constants"; import { insertTextAtCursor } from "../../../utils/ai-utils/cursorPositionInsert"; import { getAIDescriptionConfig } from "../../../utils/ai-utils/descriptionconfig"; -import { getAuthToken, isLoggedIn } from "../../../utils/checkAuthentication"; +import { getAuthToken, isLoggedIn, optLogIn } from "../../../utils/checkAuthentication"; export const DescriptionGeneratorButton = () => { const descriptionGeneratorButton = createHtmlElement("a", { @@ -27,7 +27,7 @@ const handleSubmit = async () => { try { if (!(await isLoggedIn())) { - return window.open(SUPABASE_LOGIN_URL, "_blank"); + return void optLogIn(); } if (!logo || !button) { diff --git a/src/utils/checkAuthentication.ts b/src/utils/checkAuthentication.ts index 51741f80..89c42cf2 100644 --- a/src/utils/checkAuthentication.ts +++ b/src/utils/checkAuthentication.ts @@ -1,9 +1,9 @@ import { OPEN_SAUCED_AUTH_TOKEN_KEY, - OPEN_SAUCED_OPTED_LOG_OUT_KEY, SUPABASE_LOGIN_URL, + OPEN_SAUCED_INSIGHTS_DOMAIN, + OPEN_SAUCED_OPTED_LOG_OUT_KEY, SUPABASE_LOGIN_URL, SUPABASE_PKCE_VERIFIER_COOKIE_NAME, } from "../constants"; - export const isLoggedIn = async (): Promise => Object.entries(await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY)).length !== 0; export const getAuthToken = async (): Promise => (await chrome.storage.sync.get(OPEN_SAUCED_AUTH_TOKEN_KEY))[OPEN_SAUCED_AUTH_TOKEN_KEY]; @@ -13,14 +13,66 @@ export const optLogOut = () => { void chrome.storage.local.set({ [OPEN_SAUCED_OPTED_LOG_OUT_KEY]: true }); }; -export const optLogIn = () => { +export const optLogIn = async () => { if (typeof window === "undefined") { return; } void chrome.storage.local.set({ [OPEN_SAUCED_OPTED_LOG_OUT_KEY]: false }); - window.open(SUPABASE_LOGIN_URL, "_blank"); + + const verifier = generatePKCEVerifier(); + const challenge = await generatePKCEChallenge(verifier); + + const loginURL = new URL(SUPABASE_LOGIN_URL); + + loginURL.searchParams.append("provider", "github"); + loginURL.searchParams.append( + "redirect_to", + `https://${OPEN_SAUCED_INSIGHTS_DOMAIN}`, + ); + loginURL.searchParams.append("code_challenge_method", "s256"); + loginURL.searchParams.append("code_challenge", challenge); + + await chrome.cookies.set({ + url: `https://${OPEN_SAUCED_INSIGHTS_DOMAIN}`, + name: SUPABASE_PKCE_VERIFIER_COOKIE_NAME, + value: verifier, + }); + window.open(loginURL, "_blank"); }; export const hasOptedLogOut = async (): Promise => (await chrome.storage.local.get(OPEN_SAUCED_OPTED_LOG_OUT_KEY))[OPEN_SAUCED_OPTED_LOG_OUT_KEY] === true; export const removeAuthTokenFromStorage = async (): Promise => chrome.storage.sync.remove(OPEN_SAUCED_AUTH_TOKEN_KEY); + +// Custom browser only PKCE implementation based on https://github.com/supabase/gotrue-js + +const dec2hex = (dec: number) => (`0${dec.toString(16)}`).substring(-2); + +const generatePKCEVerifier = () => { + const verifierLength = 56; + const array = new Uint32Array(verifierLength); + + crypto.getRandomValues(array); + return Array.from(array, dec2hex).join(""); +}; + +const sha256 = async (randomString: string) => { + const encoder = (new TextEncoder); + const encodedData = encoder.encode(randomString); + const hash = await window.crypto.subtle.digest("SHA-256", encodedData); + const bytes = new Uint8Array(hash); + + return Array.from(bytes) + .map(c => String.fromCharCode(c)) + .join(""); +}; + +const base64urlencode = (str: string) => btoa(str).replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/[=]+$/, ""); + +const generatePKCEChallenge = async (verifier: string) => { + const hashed = await sha256(verifier); + + return base64urlencode(hashed); +};