From 66820b3f6522eff5044645455063e2d6f9cb5337 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Tue, 6 Feb 2024 22:59:14 -0500 Subject: [PATCH 01/13] Allow users to authenticate with Google OAuth - step1 get google info --- functions/google.ts | 72 +++++++++++++++++++++++++++++++++++++++ src/components/Header.tsx | 13 +++++++ src/router.tsx | 20 ++++++++++- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 functions/google.ts diff --git a/functions/google.ts b/functions/google.ts new file mode 100644 index 00000000..bb7d40b3 --- /dev/null +++ b/functions/google.ts @@ -0,0 +1,72 @@ +// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#redirecting +export async function oauthSignIn() { + // export async function oauthSignIn(CLIENT_ID: string,REDIRECT_URI: string) { + + // Google's OAuth 2.0 endpoint for requesting an access token + const oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + + // Create
element to submit parameters to OAuth 2.0 endpoint. + const form = document.createElement("form"); + form.setAttribute("method", "GET"); // Send as a GET request. + form.setAttribute("action", oauth2Endpoint); + + // Parameters to pass to OAuth 2.0 endpoint. + const params = { + client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", // This is my test accout, can be used in testing on http://localhost:5173/ + redirect_uri: "http://localhost:5173", + response_type: "token", + scope: "profile email", + include_granted_scopes: "true", + state: "pass-through value", + }; + + // Add form parameters as hidden input values. + for (const p in params) { + const input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", p); + input.setAttribute("value", params[p]); + form.appendChild(input); + } + + // Add form to page and submit it to open the OAuth 2.0 endpoint. + document.body.appendChild(form); + form.submit(); +} + +export async function requestUserInfo(accessToken: string) { + const xhr = new XMLHttpRequest(); + const url = `https://www.googleapis.com/drive/v3/about?fields=user&access_token=${accessToken}`; + xhr.open("Get", url); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + console.log(response); + const { login, name, avatar_url } = response; + const userInfo = { + username: login, + name: name ?? login, + avatarUrl: avatar_url, + }; + console.log(userInfo); + } else if (xhr.readyState === 4 && xhr.status === 401) { + // Token invalid, so prompt for user permission. + oauthSignIn(); + } + }; + + // xhr.onreadystatechange = function () { + // console.log(xhr.response); + // }; + xhr.send(null); + + // this will be used later to create user + // const { login, name, avatar_url } = (await res.json()) as { + // login: string; + // name: string | null; + // avatar_url: string; + // }; + + // return { username: login, name: name ?? login, avatarUrl: avatar_url }; +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 05f7611c..c2ee7c55 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -20,12 +20,14 @@ import { } from "@chakra-ui/react"; import { BiSun, BiMoon, BiMenu } from "react-icons/bi"; import { BsGithub } from "react-icons/bs"; +import { FcGoogle } from "react-icons/fc"; import { TbSearch } from "react-icons/tb"; import { Form } from "react-router-dom"; import PreferencesModal from "./PreferencesModal"; import DefaultSystemPromptModal from "./DefaultSystemPromptModal"; import { useUser } from "../hooks/use-user"; +import { oauthSignIn } from "../../functions/google"; type HeaderProps = { chatId?: string; @@ -136,6 +138,17 @@ function Header({ chatId, inputPromptRef, searchText, onToggleSidebar }: HeaderP )} + {/* Google login */} + {/* { + oauthSignIn(); + console.log("google"); + }} + > + <> + Sign in with Google + + */} , From 4e1ea26dce9efc391a25345d6f156cf6dac9f6ac Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Wed, 7 Feb 2024 12:54:40 -0500 Subject: [PATCH 02/13] revise build error --- functions/google.ts | 2 +- src/components/Header.tsx | 4 ++-- src/router.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/google.ts b/functions/google.ts index bb7d40b3..de226158 100644 --- a/functions/google.ts +++ b/functions/google.ts @@ -11,7 +11,7 @@ export async function oauthSignIn() { form.setAttribute("action", oauth2Endpoint); // Parameters to pass to OAuth 2.0 endpoint. - const params = { + const params: { [key: string]: string } = { client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", // This is my test accout, can be used in testing on http://localhost:5173/ redirect_uri: "http://localhost:5173", response_type: "token", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index c2ee7c55..703804ce 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -20,14 +20,14 @@ import { } from "@chakra-ui/react"; import { BiSun, BiMoon, BiMenu } from "react-icons/bi"; import { BsGithub } from "react-icons/bs"; -import { FcGoogle } from "react-icons/fc"; +// import { FcGoogle } from "react-icons/fc"; import { TbSearch } from "react-icons/tb"; import { Form } from "react-router-dom"; import PreferencesModal from "./PreferencesModal"; import DefaultSystemPromptModal from "./DefaultSystemPromptModal"; import { useUser } from "../hooks/use-user"; -import { oauthSignIn } from "../../functions/google"; +// import { oauthSignIn } from "../../functions/google"; type HeaderProps = { chatId?: string; diff --git a/src/router.tsx b/src/router.tsx index 4b9f668a..fd99ebac 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -31,7 +31,7 @@ export default createBrowserRouter([ "GET", "https://www.googleapis.com/drive/v3/about?fields=user&" + "access_token=" + accessToken ); - xhr.onreadystatechange = function (e) { + xhr.onreadystatechange = function () { console.log(xhr.response); }; xhr.send(null); From c276e3f91ac38a87c9d7e59f2d5e87f395058a09 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Sat, 10 Feb 2024 11:21:36 -0500 Subject: [PATCH 03/13] g tmp --- functions/api/login.ts | 2 + functions/google.ts | 250 +++++++++++++++++++++++++++++--------- src/components/Header.tsx | 8 +- 3 files changed, 200 insertions(+), 60 deletions(-) diff --git a/functions/api/login.ts b/functions/api/login.ts index 9677392b..917bd8dd 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -102,6 +102,8 @@ export async function handleDevLogin({ } export const onRequestGet: PagesFunction = async ({ request, env }) => { + console.log("??????"); + console.log(request); const { CLIENT_ID, CLIENT_SECRET, JWT_SECRET } = env; const reqUrl = new URL(request.url); const code = reqUrl.searchParams.get("code"); diff --git a/functions/google.ts b/functions/google.ts index de226158..c20da5c8 100644 --- a/functions/google.ts +++ b/functions/google.ts @@ -1,72 +1,210 @@ -// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#redirecting +import { buildUrl } from "./utils"; + export async function oauthSignIn() { - // export async function oauthSignIn(CLIENT_ID: string,REDIRECT_URI: string) { + console.log("run this"); - // Google's OAuth 2.0 endpoint for requesting an access token - const oauth2Endpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + const url = buildUrl("https://accounts.google.com/o/oauth2/v2/auth", { + client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", + redirect_uri: "http://localhost:5173", + response_type: "code", + scope: "profile email", + }); + // Redirect the user to the OAuth endpoint + window.location.href = url; +} - // Create element to submit parameters to OAuth 2.0 endpoint. - const form = document.createElement("form"); - form.setAttribute("method", "GET"); // Send as a GET request. - form.setAttribute("action", oauth2Endpoint); +export async function oauthSignIn1() { + console.log("??????3"); + // const url = buildUrl("https://accounts.google.com/o/oauth2/token", { + // client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", + // client_secret: "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", + // // code: code, + // code: "4/AfJohXkCm2BE96kgFP4u0cNRcuYzY6i-1PRiVnKd-tzCxeFj8c6BFpg5FVtxFbuvSkoVRQ", + // grant_type: "authorization_code", + // redirect_uri: "http://localhost:5173", + // }); + // console.log(url); - // Parameters to pass to OAuth 2.0 endpoint. - const params: { [key: string]: string } = { - client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", // This is my test accout, can be used in testing on http://localhost:5173/ + // const res = await fetch(url, { + // method: "POST", + // headers: { + // "Content-Type": "application/x-www-form-urlencoded", + // }, + // }); + const url = "https://accounts.google.com/o/oauth2/token"; + const data = { + client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", + client_secret: "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", redirect_uri: "http://localhost:5173", - response_type: "token", - scope: "profile email", - include_granted_scopes: "true", - state: "pass-through value", + grant_type: "authorization_code", + code: "4/AfJohXmf_ReIb8mOkU6jJa5mzQK0LrS7cZmUX0YM42uaDFWDfVEATlRRJDvckZAiWkp2WA", }; - // Add form parameters as hidden input values. - for (const p in params) { - const input = document.createElement("input"); - input.setAttribute("type", "hidden"); - input.setAttribute("name", p); - input.setAttribute("value", params[p]); - form.appendChild(input); + const searchParams = new URLSearchParams(); + + for (const prop in data) { + searchParams.set(prop, data[prop]); } - // Add form to page and submit it to open the OAuth 2.0 endpoint. - document.body.appendChild(form); - form.submit(); -} + fetch(url, { + method: "POST", + body: searchParams.toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((response) => response.json()) + .then((data) => console.log(data)) + .catch((error) => { + console.error("Error:", error); + }); -export async function requestUserInfo(accessToken: string) { - const xhr = new XMLHttpRequest(); - const url = `https://www.googleapis.com/drive/v3/about?fields=user&access_token=${accessToken}`; - xhr.open("Get", url); - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && xhr.status === 200) { - const response = JSON.parse(xhr.responseText); - console.log(response); - const { login, name, avatar_url } = response; - const userInfo = { - username: login, - name: name ?? login, - avatarUrl: avatar_url, - }; - console.log(userInfo); - } else if (xhr.readyState === 4 && xhr.status === 401) { - // Token invalid, so prompt for user permission. - oauthSignIn(); - } - }; + // console.log(res); + + const url1 = buildUrl("https://oauth2.googleapis.com/token", { + client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", + client_secret: "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", + // code: code, + // code: "4%2F0AfJohXkz17cEGwB5s7R2UZtpMVQdbwYsB9ema0MXAO6zO4GEYJuXD4hoScLQbDrRNIB-_A", + code: "4/AfJohXkyC-cwL3qyWkyVI_ie61MbhQqNSu3uJdvmB9X4E2-wIiTUWbbrtVukb5qtz8DTaQ", + grant_type: "authorization_code", + redirect_uri: "http://localhost:5173", + }); - // xhr.onreadystatechange = function () { - // console.log(xhr.response); + // const response = await fetch(url1, { + // method: "POST", + // headers: { + // "User-Agent": "chatcraft.org", + // Accept: "application/json", + // }, + // }); + + // const url = "https://oauth2.googleapis.com/token"; + // const data = { + // client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", + // client_secret: "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", + // redirect_uri: "http://localhost:5173", + // grant_type: "authorization_code", + // code: "4/AfJohXkz17cEGwB5s7R2UZtpMVQdbwYsB9ema0MXAO6zO4GEYJuXD4hoScLQbDrRNIB-_A", // }; - xhr.send(null); - // this will be used later to create user - // const { login, name, avatar_url } = (await res.json()) as { - // login: string; - // name: string | null; - // avatar_url: string; + // const url = "https://oauth2.googleapis.com/token"; + + // const data = new URLSearchParams(); + // data.append("grant_type", "authorization_code"); + // data.append( + // "client_id", + // "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com" + // ); + // data.append("client_secret", "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"); + // data.append("redirect_uri", "https://openidconnect.net/callback"); + // data.append("code", "4/AfJohXkz17cEGwB5s7R2UZtpMVQdbwYsB9ema0MXAO6zO4GEYJuXD4hoScLQbDrRNIB-_A"); + + // fetch(url, { + // method: "POST", + // headers: { + // "Content-Type": "application/x-www-form-urlencoded", + // }, + // body: data, + // }) + // .then((response) => response.json()) + // .then((data) => { + // console.log("Response:", data); + // }) + // .catch((error) => { + // console.error("Error:", error); + // }); + // const oauth2Client = new google.auth.OAuth2( + // "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", + // "GOCSPX - uklMNha5jyo4gKzP4P_cAgUdwAYO", + // "https://openidconnect.net/callback" + // ); + + // const { tokens } = await oauth2Client.getToken( + // "4/AfJohXlD_eEgmjRqZWMFfBTbj-rU_pQ6rerfmmryuXfsjFp3CSuzttSuAI5Q2K-PPGwRRw" + // ); + // console.log(tokens); + + // const data = { + // client_id: "YOUR_CLIENT_ID", + // client_secret: "YOUR_CLIENT_SECRET", + // // code: "4/AfJohXlEk1M4H91lucF0kqv8kErdpUlFO2b0e1kXkhLzwNx_1Ut20eZ04tMZERisPs_5GA", + // code: "4%2F0AfJohXlEk1M4H91lucF0kqv8kErdpUlFO2b0e1kXkhLzwNx_1Ut20eZ04tMZERisPs_5GA", + // grant_type: "authorization_code", + // redirect_uri: "YOUR_REDIRECT_URI", // }; - // return { username: login, name: name ?? login, avatarUrl: avatar_url }; + // const response = await axios.post("https://oauth2.googleapis.com/token", qs.stringify(data), { + // headers: { + // "Content-Type": "application/x-www-form-urlencoded", + // }, + // }); + + console.log("response:"); + // console.log(response); + // return response.data.access_token; } + +//localhost:5173/?code=4%2F0AfJohXlEk1M4H91lucF0kqv8kErdpUlFO2b0e1kXkhLzwNx_1Ut20eZ04tMZERisPs_5GA&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=0&prompt=consent + +//localhost:5173/?code=4%2F0AfJohXlD_eEgmjRqZWMFfBTbj-rU_pQ6rerfmmryuXfsjFp3CSuzttSuAI5Q2K-PPGwRRw +// &scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none + +// async function getAccessToken(code: string) { +// const url = buildUrl("https://oauth2.googleapis.com/token", { +// client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", +// client_secret: "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", +// // code: code, +// code: "4/0AfJohXl9FH_JjS5wPFS1aOG85qcD0FBmmPDNjhLKGm7qZKB7VWvOmE5hrZWD-bE9PB7HhQ", +// grant_type: "authorization_code", +// redirect_uri: "http://localhost:5173", +// }); + +// const response = await fetch(url, { +// headers: { +// "Content-Type": "application/x-www-form-urlencoded", +// }, +// }); + +// console.log(response); +// return response.data.access_token; +// } + +// export async function requestUserInfo(accessToken: string) { +// const xhr = new XMLHttpRequest(); +// const url = `https://www.googleapis.com/drive/v3/about?fields=user&access_token=${accessToken}`; +// xhr.open("Get", url); +// // xhr.open( +// // "GET", +// // "https://www.googleapis.com/drive/v3/about?fields=user&" + "access_token=" + accessToken +// // ); +// xhr.onreadystatechange = function () { +// if (xhr.readyState === 4 && xhr.status === 200) { +// const response = JSON.parse(xhr.responseText); +// console.log(response); +// const { login, name, avatar_url } = response; +// const userInfo = { +// username: login, +// name: name ?? login, +// avatarUrl: avatar_url, +// }; +// console.log(userInfo); +// } else if (xhr.readyState === 4 && xhr.status === 401) { +// // Token invalid, so prompt for user permission. +// oauthSignIn(); +// } +// }; + +// // xhr.onreadystatechange = function () { +// // console.log(xhr.response); +// // }; +// xhr.send(null); + +// // const { login, name, avatar_url } = (await res.json()) as { +// // login: string; +// // name: string | null; +// // avatar_url: string; +// // }; + +// // return { username: login, name: name ?? login, avatarUrl: avatar_url }; +// } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 703804ce..bb4c59c8 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -20,14 +20,14 @@ import { } from "@chakra-ui/react"; import { BiSun, BiMoon, BiMenu } from "react-icons/bi"; import { BsGithub } from "react-icons/bs"; -// import { FcGoogle } from "react-icons/fc"; +import { FcGoogle } from "react-icons/fc"; import { TbSearch } from "react-icons/tb"; import { Form } from "react-router-dom"; import PreferencesModal from "./PreferencesModal"; import DefaultSystemPromptModal from "./DefaultSystemPromptModal"; import { useUser } from "../hooks/use-user"; -// import { oauthSignIn } from "../../functions/google"; +import { oauthSignIn } from "../../functions/google"; type HeaderProps = { chatId?: string; @@ -139,7 +139,7 @@ function Header({ chatId, inputPromptRef, searchText, onToggleSidebar }: HeaderP )} {/* Google login */} - {/* { oauthSignIn(); console.log("google"); @@ -148,7 +148,7 @@ function Header({ chatId, inputPromptRef, searchText, onToggleSidebar }: HeaderP <> Sign in with Google - */} + Date: Tue, 13 Feb 2024 22:28:24 -0500 Subject: [PATCH 04/13] using server-side google APIs --- functions/api/login.ts | 186 +++++++++++++++++++++++++--- functions/google.test.ts | 69 +++++++++++ functions/google.ts | 246 ++++++++------------------------------ src/components/Header.tsx | 63 ++++++---- src/hooks/use-user.tsx | 8 +- src/router.tsx | 19 --- 6 files changed, 336 insertions(+), 255 deletions(-) create mode 100644 functions/google.test.ts diff --git a/functions/api/login.ts b/functions/api/login.ts index 917bd8dd..9824aa4e 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -1,5 +1,10 @@ import { buildUrl, createResourcesForEnv } from "../utils"; import { requestAccessToken, requestUserInfo, requestDevUserInfo } from "../github"; +import { + requestGoogleAccessToken, + requestGoogleDevUserInfo, + requestGoogleUserInfo, +} from "../google"; import { TokenProvider } from "../token-provider"; interface Env { @@ -7,8 +12,14 @@ interface Env { CLIENT_ID: string; CLIENT_SECRET: string; JWT_SECRET: string; + GOOGLE_CLIENT_ID: string; + GOOGLE_REDIRECT_URI: string; + GOOGLE_RESPONSE_TYPE: string; + GOOGLE_SCOPE: string; } +let provider: string | null = ""; + // Authenticate the user with GitHub, then create a JWT for use in ChatCraft. // We store the token in a secure, HTTP-only cookie. export async function handleProdLogin({ @@ -101,26 +112,173 @@ export async function handleDevLogin({ } } +export async function handleGoogleProdLogin({ + code, + chatId, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + JWT_SECRET, + tokenProvider, + appUrl, +}: { + code: string | null; + chatId: string | null; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + GOOGLE_REDIRECT_URI: string; + GOOGLE_RESPONSE_TYPE: string; + GOOGLE_SCOPE: string; + JWT_SECRET: string; + tokenProvider: TokenProvider; + appUrl: string; +}) { + // If we're missing the code, redirect to the Google Auth UI + if (!code) { + // const state = JSON.stringify({ provider: "google", chatId: chatId }); + const url = buildUrl( + "https://accounts.google.com/o/oauth2/v2/auth", + // If there's a chatId, piggy-back it on the request as state + chatId + ? { + client_id: GOOGLE_CLIENT_ID, + redirect_uri: GOOGLE_REDIRECT_URI, + response_type: GOOGLE_RESPONSE_TYPE, + scope: GOOGLE_SCOPE, + state: chatId, + } + : { + client_id: GOOGLE_CLIENT_ID, + redirect_uri: GOOGLE_REDIRECT_URI, + response_type: GOOGLE_RESPONSE_TYPE, + scope: GOOGLE_SCOPE, + } + ); + return Response.redirect(url, 302); + } + + // Otherwise, exchange the code for an access_token, then get user info + // and use that to create JWTs for ChatCraft. + try { + const googleAccessToken = await requestGoogleAccessToken( + code, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI + ); + const user = await requestGoogleUserInfo(googleAccessToken); + // User info goes in a non HTTP-Only cookie that browser can read + const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); + // API authorization goes in an HTTP-Only cookie that only functions can read + const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); + + // Return to the root or a specific chat if we have an id + const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; + + return new Response(null, { + status: 302, + headers: new Headers([ + ["Location", url], + ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], + ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], + ]), + }); + } catch (err) { + console.error(err); + return Response.redirect(`https://chatcraft.org/?google_login_error`, 302); + } +} + +// In development, we simulate a Google login. +export async function handleGoogleDevLogin({ + chatId, + JWT_SECRET, + tokenProvider, + appUrl, +}: { + chatId: string | null; + JWT_SECRET: string; + tokenProvider: TokenProvider; + appUrl: string; +}) { + try { + const user = requestGoogleDevUserInfo(); + // User info goes in a non HTTP-Only cookie that browser can read + const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); + // API authorization goes in an HTTP-Only cookie that only functions can read + const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); + + // Return to the root or a specific chat if we have an id + const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; + + return new Response(null, { + status: 302, + headers: new Headers([ + ["Location", url], + ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], + ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], + ]), + }); + } catch (err) { + console.error(err); + return Response.redirect(`/?google_login_error`, 302); + } +} + export const onRequestGet: PagesFunction = async ({ request, env }) => { - console.log("??????"); - console.log(request); - const { CLIENT_ID, CLIENT_SECRET, JWT_SECRET } = env; + const { + CLIENT_ID, + CLIENT_SECRET, + JWT_SECRET, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + } = env; + const reqUrl = new URL(request.url); + // Determine the login provider + provider = reqUrl.searchParams.get("provider") ? reqUrl.searchParams.get("provider") : provider; const code = reqUrl.searchParams.get("code"); // Include ?chat_id=... to redirect back to a given chat in the client. GitHub will // return it back to us via ?state=... const chatId = reqUrl.searchParams.get("chat_id") || reqUrl.searchParams.get("state"); const { appUrl, isDev, tokenProvider } = createResourcesForEnv(env.ENVIRONMENT, request.url); - return isDev - ? handleDevLogin({ chatId, JWT_SECRET, tokenProvider, appUrl }) - : handleProdLogin({ - code, - chatId, - CLIENT_ID, - CLIENT_SECRET, - JWT_SECRET, - tokenProvider, - appUrl, - }); + if (provider === "google") { + return isDev + ? handleGoogleDevLogin({ + chatId, + JWT_SECRET, + tokenProvider, + appUrl, + }) + : handleGoogleProdLogin({ + code, + chatId, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + JWT_SECRET, + tokenProvider, + appUrl, + }); + } else { + return isDev + ? handleDevLogin({ chatId, JWT_SECRET, tokenProvider, appUrl }) + : handleProdLogin({ + code, + chatId, + CLIENT_ID, + CLIENT_SECRET, + JWT_SECRET, + tokenProvider, + appUrl, + }); + } }; diff --git a/functions/google.test.ts b/functions/google.test.ts new file mode 100644 index 00000000..d0bcb211 --- /dev/null +++ b/functions/google.test.ts @@ -0,0 +1,69 @@ +import { describe, test, expect } from "vitest"; + +import { requestGoogleAccessToken, requestGoogleUserInfo } from "./google"; + +export const googleMocks = () => ({ + "google.com": (mockAccessToken = "google_access_token...") => { + // Mock the call to GitHub's OAuth login endpoint + const fetchMock = getMiniflareFetchMock(); + fetchMock.disableNetConnect(); + const origin = fetchMock.get("https://accounts.google.com"); + origin + .intercept({ method: "POST", path: /^\/o\/oauth2\/token.+/ }) + .reply(200, { access_token: mockAccessToken }); + return origin; + }, + "www.googleapis.com": ({ + email, + name, + picture, + }: { + email: string; + name: string; + picture: string; + }) => { + const fetchMock = getMiniflareFetchMock(); + fetchMock.disableNetConnect(); + const origin = fetchMock.get("https://www.googleapis.com"); + origin + .intercept({ method: "GET", path: "/oauth2/v1/userinfo" }) + .reply(200, { email, name, picture }); + return origin; + }, + all(mockAccessToken = "googleo_access_token...") { + this["google.com"](mockAccessToken); + // Use default user details + this["www.googleapis.com"]({ email: "email", name: "name", picture: "picture" }); + }, +}); + +describe("google.ts", () => { + const mockAccessToken = "gho_access_token..."; + + test("requestAccessToken()", async () => { + const env = getMiniflareBindings(); + + // Mock the call to GitHub's OAuth login endpoint + const mocks = googleMocks(); + mocks["google.com"](mockAccessToken); + + const accessToken = await requestGoogleAccessToken( + "code", + env.CLIENT_ID, + env.CLIENT_SECRET, + env.GOOGLE_REDIRECT_URI + ); + expect(accessToken).toEqual(mockAccessToken); + }); + + test("requestGoogleUserInfo()", async () => { + // Mock the call to Google API /user endpoint + const mocks = googleMocks(); + mocks["www.googleapis.com"]({ email: "email", name: "name", picture: "picture" }); + + const user: User = await requestGoogleUserInfo(mockAccessToken); + expect(user.username).toEqual("email"); + expect(user.name).toEqual("name"); + expect(user.avatarUrl).toEqual("picture"); + }); +}); diff --git a/functions/google.ts b/functions/google.ts index c20da5c8..8bdb05d1 100644 --- a/functions/google.ts +++ b/functions/google.ts @@ -1,210 +1,70 @@ import { buildUrl } from "./utils"; -export async function oauthSignIn() { - console.log("run this"); - - const url = buildUrl("https://accounts.google.com/o/oauth2/v2/auth", { - client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", - redirect_uri: "http://localhost:5173", - response_type: "code", - scope: "profile email", - }); - // Redirect the user to the OAuth endpoint - window.location.href = url; -} - -export async function oauthSignIn1() { - console.log("??????3"); - // const url = buildUrl("https://accounts.google.com/o/oauth2/token", { - // client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", - // client_secret: "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", - // // code: code, - // code: "4/AfJohXkCm2BE96kgFP4u0cNRcuYzY6i-1PRiVnKd-tzCxeFj8c6BFpg5FVtxFbuvSkoVRQ", - // grant_type: "authorization_code", - // redirect_uri: "http://localhost:5173", - // }); - // console.log(url); - - // const res = await fetch(url, { - // method: "POST", - // headers: { - // "Content-Type": "application/x-www-form-urlencoded", - // }, - // }); - const url = "https://accounts.google.com/o/oauth2/token"; - const data = { - client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", - client_secret: "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", - redirect_uri: "http://localhost:5173", +// https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code +export async function requestGoogleAccessToken( + code: string, + CLIENT_ID: string, + CLIENT_SECRET: string, + GOOGLE_REDIRECT_URI: string +) { + const url = buildUrl("https://accounts.google.com/o/oauth2/token", { + code: code, + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + redirect_uri: GOOGLE_REDIRECT_URI, grant_type: "authorization_code", - code: "4/AfJohXmf_ReIb8mOkU6jJa5mzQK0LrS7cZmUX0YM42uaDFWDfVEATlRRJDvckZAiWkp2WA", - }; - - const searchParams = new URLSearchParams(); - - for (const prop in data) { - searchParams.set(prop, data[prop]); - } + }); - fetch(url, { + const res = await fetch(url, { method: "POST", - body: searchParams.toString(), headers: { - "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "chatcraft.org", }, - }) - .then((response) => response.json()) - .then((data) => console.log(data)) - .catch((error) => { - console.error("Error:", error); - }); - - // console.log(res); - - const url1 = buildUrl("https://oauth2.googleapis.com/token", { - client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", - client_secret: "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", - // code: code, - // code: "4%2F0AfJohXkz17cEGwB5s7R2UZtpMVQdbwYsB9ema0MXAO6zO4GEYJuXD4hoScLQbDrRNIB-_A", - code: "4/AfJohXkyC-cwL3qyWkyVI_ie61MbhQqNSu3uJdvmB9X4E2-wIiTUWbbrtVukb5qtz8DTaQ", - grant_type: "authorization_code", - redirect_uri: "http://localhost:5173", }); - // const response = await fetch(url1, { - // method: "POST", - // headers: { - // "User-Agent": "chatcraft.org", - // Accept: "application/json", - // }, - // }); - - // const url = "https://oauth2.googleapis.com/token"; - // const data = { - // client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", - // client_secret: "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", - // redirect_uri: "http://localhost:5173", - // grant_type: "authorization_code", - // code: "4/AfJohXkz17cEGwB5s7R2UZtpMVQdbwYsB9ema0MXAO6zO4GEYJuXD4hoScLQbDrRNIB-_A", - // }; - - // const url = "https://oauth2.googleapis.com/token"; - - // const data = new URLSearchParams(); - // data.append("grant_type", "authorization_code"); - // data.append( - // "client_id", - // "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com" - // ); - // data.append("client_secret", "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"); - // data.append("redirect_uri", "https://openidconnect.net/callback"); - // data.append("code", "4/AfJohXkz17cEGwB5s7R2UZtpMVQdbwYsB9ema0MXAO6zO4GEYJuXD4hoScLQbDrRNIB-_A"); - - // fetch(url, { - // method: "POST", - // headers: { - // "Content-Type": "application/x-www-form-urlencoded", - // }, - // body: data, - // }) - // .then((response) => response.json()) - // .then((data) => { - // console.log("Response:", data); - // }) - // .catch((error) => { - // console.error("Error:", error); - // }); - // const oauth2Client = new google.auth.OAuth2( - // "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", - // "GOCSPX - uklMNha5jyo4gKzP4P_cAgUdwAYO", - // "https://openidconnect.net/callback" - // ); - - // const { tokens } = await oauth2Client.getToken( - // "4/AfJohXlD_eEgmjRqZWMFfBTbj-rU_pQ6rerfmmryuXfsjFp3CSuzttSuAI5Q2K-PPGwRRw" - // ); - // console.log(tokens); - - // const data = { - // client_id: "YOUR_CLIENT_ID", - // client_secret: "YOUR_CLIENT_SECRET", - // // code: "4/AfJohXlEk1M4H91lucF0kqv8kErdpUlFO2b0e1kXkhLzwNx_1Ut20eZ04tMZERisPs_5GA", - // code: "4%2F0AfJohXlEk1M4H91lucF0kqv8kErdpUlFO2b0e1kXkhLzwNx_1Ut20eZ04tMZERisPs_5GA", - // grant_type: "authorization_code", - // redirect_uri: "YOUR_REDIRECT_URI", - // }; + if (!res.ok) { + throw new Error(`Failed to get Google token: ${res.status} ${await res.text()}`); + } - // const response = await axios.post("https://oauth2.googleapis.com/token", qs.stringify(data), { - // headers: { - // "Content-Type": "application/x-www-form-urlencoded", - // }, - // }); + const result = (await res.json()) as { + error?: string; + access_token: string; + }; + if (result.error) { + throw new Error(`Error in Google token response: ${result.error}`); + } - console.log("response:"); - // console.log(response); - // return response.data.access_token; + return result.access_token; } -//localhost:5173/?code=4%2F0AfJohXlEk1M4H91lucF0kqv8kErdpUlFO2b0e1kXkhLzwNx_1Ut20eZ04tMZERisPs_5GA&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid&authuser=0&prompt=consent - -//localhost:5173/?code=4%2F0AfJohXlD_eEgmjRqZWMFfBTbj-rU_pQ6rerfmmryuXfsjFp3CSuzttSuAI5Q2K-PPGwRRw -// &scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none - -// async function getAccessToken(code: string) { -// const url = buildUrl("https://oauth2.googleapis.com/token", { -// client_id: "70478082635-iaa28pt6bg1h06ooeic3vo8fgtu90trh.apps.googleusercontent.com", -// client_secret: "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", -// // code: code, -// code: "4/0AfJohXl9FH_JjS5wPFS1aOG85qcD0FBmmPDNjhLKGm7qZKB7VWvOmE5hrZWD-bE9PB7HhQ", -// grant_type: "authorization_code", -// redirect_uri: "http://localhost:5173", -// }); - -// const response = await fetch(url, { -// headers: { -// "Content-Type": "application/x-www-form-urlencoded", -// }, -// }); - -// console.log(response); -// return response.data.access_token; -// } +export async function requestGoogleUserInfo(token: string): Promise { + const res = await fetch("https://www.googleapis.com/oauth2/v1/userinfo", { + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + "User-Agent": "chatcraft.org", + }, + }); -// export async function requestUserInfo(accessToken: string) { -// const xhr = new XMLHttpRequest(); -// const url = `https://www.googleapis.com/drive/v3/about?fields=user&access_token=${accessToken}`; -// xhr.open("Get", url); -// // xhr.open( -// // "GET", -// // "https://www.googleapis.com/drive/v3/about?fields=user&" + "access_token=" + accessToken -// // ); -// xhr.onreadystatechange = function () { -// if (xhr.readyState === 4 && xhr.status === 200) { -// const response = JSON.parse(xhr.responseText); -// console.log(response); -// const { login, name, avatar_url } = response; -// const userInfo = { -// username: login, -// name: name ?? login, -// avatarUrl: avatar_url, -// }; -// console.log(userInfo); -// } else if (xhr.readyState === 4 && xhr.status === 401) { -// // Token invalid, so prompt for user permission. -// oauthSignIn(); -// } -// }; + if (!res.ok) { + throw new Error(`Failed to get Google User info: ${res.status} ${await res.text()}`); + } -// // xhr.onreadystatechange = function () { -// // console.log(xhr.response); -// // }; -// xhr.send(null); + const { email, name, picture } = (await res.json()) as { + email: string; + name: string; + picture: string; + }; -// // const { login, name, avatar_url } = (await res.json()) as { -// // login: string; -// // name: string | null; -// // avatar_url: string; -// // }; + return { username: email, name: name, avatarUrl: picture }; +} -// // return { username: login, name: name ?? login, avatarUrl: avatar_url }; -// } +// In development environments, we automatically log the user in without involving GitHub +export function requestGoogleDevUserInfo() { + return { + username: "chatcraft_dev_google", + name: "ChatCraftDevGoogle", + avatarUrl: + "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg?size=402", + }; +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bb4c59c8..37faae3b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -27,7 +27,6 @@ import { Form } from "react-router-dom"; import PreferencesModal from "./PreferencesModal"; import DefaultSystemPromptModal from "./DefaultSystemPromptModal"; import { useUser } from "../hooks/use-user"; -import { oauthSignIn } from "../../functions/google"; type HeaderProps = { chatId?: string; @@ -50,13 +49,16 @@ function Header({ chatId, inputPromptRef, searchText, onToggleSidebar }: HeaderP } = useDisclosure(); const { user, login, logout } = useUser(); - const handleLoginLogout = useCallback(() => { - if (user) { - logout(chatId); - } else { - login(chatId); - } - }, [chatId, user, login, logout]); + const handleLoginLogout = useCallback( + (provider: string) => { + if (user) { + logout(chatId); + } else { + login(provider, chatId); + } + }, + [chatId, user, login, logout] + ); return ( Settings... Default System Prompt... - - {user ? ( - "Logout" - ) : ( - <> - Sign in with GitHub - - )} - - {/* Google login */} - { - oauthSignIn(); - console.log("google"); - }} - > + {user && ( + { + handleLoginLogout(""); + }} + > + Logout + + )} + {!user && ( <> - Sign in with Google + { + handleLoginLogout("github"); + }} + > + Sign in with GitHub + + {/* Google login */} + { + handleLoginLogout("google"); + }} + > + Sign in with Google + - + )} + void; + login: (provider: string, chatId?: string) => void; logout: (chatId?: string) => Promise; }; @@ -87,8 +87,10 @@ export const UserProvider: FC<{ children: ReactNode }> = ({ children }) => { const value = { user, - login(chatId?: string) { - const loginUrl = chatId ? `/api/login?chat_id=${chatId}` : `/api/login`; + login(provider: string, chatId?: string) { + const loginUrl = chatId + ? `/api/login?provider=${provider}&chat_id=${chatId}` + : `/api/login?provider=${provider}`; window.location.href = loginUrl; }, logout, diff --git a/src/router.tsx b/src/router.tsx index fd99ebac..69468191 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -17,25 +17,6 @@ export default createBrowserRouter([ { path: "/", async loader() { - // Because google will redirect_uri to "/" with accessToken - const requestedURI = location.pathname; - // Accessing anchor values - const anchor = location.hash.substring(1); // Exclude the '#' character - console.log("Requested URI:", requestedURI); - console.log("Anchor:", anchor); - const match = anchor.match(/access_token=([^&]*)/); - const accessToken = match ? match[1] : null; - console.log("accessToken:", accessToken); - const xhr = new XMLHttpRequest(); - xhr.open( - "GET", - "https://www.googleapis.com/drive/v3/about?fields=user&" + "access_token=" + accessToken - ); - xhr.onreadystatechange = function () { - console.log(xhr.response); - }; - xhr.send(null); - try { const recentChat = await db.chats.orderBy("date").last(); if (recentChat) { From 62dd536ad6424e890158e333f123d38910ab6a27 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Tue, 13 Feb 2024 22:58:59 -0500 Subject: [PATCH 05/13] revise .dev.vars file --- .dev.vars | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.dev.vars b/.dev.vars index dad8cdee..c5e05539 100644 --- a/.dev.vars +++ b/.dev.vars @@ -2,3 +2,8 @@ # https://developers.cloudflare.com/pages/platform/functions/bindings/#interact-with-your-environment-variables-locally ENVIRONMENT=development JWT_SECRET=secret +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=https://chatcraft.org/api/login/ +GOOGLE_RESPONSE_TYPE=code +GOOGLE_SCOPE=profile email From f8e2d230ea6156c9a9777230132f32e5caf89483 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Tue, 13 Feb 2024 23:00:16 -0500 Subject: [PATCH 06/13] revise .dev.vars file --- .dev.vars | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.dev.vars b/.dev.vars index dad8cdee..c5e05539 100644 --- a/.dev.vars +++ b/.dev.vars @@ -2,3 +2,8 @@ # https://developers.cloudflare.com/pages/platform/functions/bindings/#interact-with-your-environment-variables-locally ENVIRONMENT=development JWT_SECRET=secret +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=https://chatcraft.org/api/login/ +GOOGLE_RESPONSE_TYPE=code +GOOGLE_SCOPE=profile email From 379345995d5decbf85c11b9dc9e1d769bf52491e Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Wed, 14 Feb 2024 21:56:56 -0500 Subject: [PATCH 07/13] move github and google specific methods to function file --- functions/api/login.ts | 275 ++++---------------------------------- functions/github.ts | 115 ++++++++++++++++ functions/google.ts | 149 +++++++++++++++++++++ src/components/Header.tsx | 5 +- src/router.tsx | 1 + 5 files changed, 295 insertions(+), 250 deletions(-) diff --git a/functions/api/login.ts b/functions/api/login.ts index 9824aa4e..83d66cfe 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -1,11 +1,6 @@ -import { buildUrl, createResourcesForEnv } from "../utils"; -import { requestAccessToken, requestUserInfo, requestDevUserInfo } from "../github"; -import { - requestGoogleAccessToken, - requestGoogleDevUserInfo, - requestGoogleUserInfo, -} from "../google"; -import { TokenProvider } from "../token-provider"; +import { createResourcesForEnv } from "../utils"; +import { handleGithubLogin } from "../github"; +import { handleGoogleLogin } from "../google"; interface Env { ENVIRONMENT: string; @@ -19,214 +14,6 @@ interface Env { } let provider: string | null = ""; - -// Authenticate the user with GitHub, then create a JWT for use in ChatCraft. -// We store the token in a secure, HTTP-only cookie. -export async function handleProdLogin({ - code, - chatId, - CLIENT_ID, - CLIENT_SECRET, - JWT_SECRET, - tokenProvider, - appUrl, -}: { - code: string | null; - chatId: string | null; - CLIENT_ID: string; - CLIENT_SECRET: string; - JWT_SECRET: string; - tokenProvider: TokenProvider; - appUrl: string; -}) { - // If we're missing the code, redirect to the GitHub Auth UI - if (!code) { - const url = buildUrl( - "https://github.com/login/oauth/authorize", - // If there's a chatId, piggy-back it on the request as state - chatId ? { client_id: CLIENT_ID, state: chatId } : { client_id: CLIENT_ID } - ); - return Response.redirect(url, 302); - } - - // Otherwise, exchange the code for an access_token, then get user info - // and use that to create JWTs for ChatCraft. - try { - const ghAccessToken = await requestAccessToken(code, CLIENT_ID, CLIENT_SECRET); - const user = await requestUserInfo(ghAccessToken); - // User info goes in a non HTTP-Only cookie that browser can read - const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); - // API authorization goes in an HTTP-Only cookie that only functions can read - const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); - - // Return to the root or a specific chat if we have an id - const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; - - return new Response(null, { - status: 302, - headers: new Headers([ - ["Location", url], - ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], - ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], - ]), - }); - } catch (err) { - console.error(err); - return Response.redirect(`https://chatcraft.org/?github_login_error`, 302); - } -} - -// In development, we simulate a GitHub login. -export async function handleDevLogin({ - chatId, - JWT_SECRET, - tokenProvider, - appUrl, -}: { - chatId: string | null; - JWT_SECRET: string; - tokenProvider: TokenProvider; - appUrl: string; -}) { - try { - const user = requestDevUserInfo(); - // User info goes in a non HTTP-Only cookie that browser can read - const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); - // API authorization goes in an HTTP-Only cookie that only functions can read - const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); - - // Return to the root or a specific chat if we have an id - const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; - - return new Response(null, { - status: 302, - headers: new Headers([ - ["Location", url], - ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], - ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], - ]), - }); - } catch (err) { - console.error(err); - return Response.redirect(`/?github_login_error`, 302); - } -} - -export async function handleGoogleProdLogin({ - code, - chatId, - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - GOOGLE_REDIRECT_URI, - GOOGLE_RESPONSE_TYPE, - GOOGLE_SCOPE, - JWT_SECRET, - tokenProvider, - appUrl, -}: { - code: string | null; - chatId: string | null; - GOOGLE_CLIENT_ID: string; - GOOGLE_CLIENT_SECRET: string; - GOOGLE_REDIRECT_URI: string; - GOOGLE_RESPONSE_TYPE: string; - GOOGLE_SCOPE: string; - JWT_SECRET: string; - tokenProvider: TokenProvider; - appUrl: string; -}) { - // If we're missing the code, redirect to the Google Auth UI - if (!code) { - // const state = JSON.stringify({ provider: "google", chatId: chatId }); - const url = buildUrl( - "https://accounts.google.com/o/oauth2/v2/auth", - // If there's a chatId, piggy-back it on the request as state - chatId - ? { - client_id: GOOGLE_CLIENT_ID, - redirect_uri: GOOGLE_REDIRECT_URI, - response_type: GOOGLE_RESPONSE_TYPE, - scope: GOOGLE_SCOPE, - state: chatId, - } - : { - client_id: GOOGLE_CLIENT_ID, - redirect_uri: GOOGLE_REDIRECT_URI, - response_type: GOOGLE_RESPONSE_TYPE, - scope: GOOGLE_SCOPE, - } - ); - return Response.redirect(url, 302); - } - - // Otherwise, exchange the code for an access_token, then get user info - // and use that to create JWTs for ChatCraft. - try { - const googleAccessToken = await requestGoogleAccessToken( - code, - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - GOOGLE_REDIRECT_URI - ); - const user = await requestGoogleUserInfo(googleAccessToken); - // User info goes in a non HTTP-Only cookie that browser can read - const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); - // API authorization goes in an HTTP-Only cookie that only functions can read - const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); - - // Return to the root or a specific chat if we have an id - const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; - - return new Response(null, { - status: 302, - headers: new Headers([ - ["Location", url], - ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], - ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], - ]), - }); - } catch (err) { - console.error(err); - return Response.redirect(`https://chatcraft.org/?google_login_error`, 302); - } -} - -// In development, we simulate a Google login. -export async function handleGoogleDevLogin({ - chatId, - JWT_SECRET, - tokenProvider, - appUrl, -}: { - chatId: string | null; - JWT_SECRET: string; - tokenProvider: TokenProvider; - appUrl: string; -}) { - try { - const user = requestGoogleDevUserInfo(); - // User info goes in a non HTTP-Only cookie that browser can read - const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); - // API authorization goes in an HTTP-Only cookie that only functions can read - const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); - - // Return to the root or a specific chat if we have an id - const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; - - return new Response(null, { - status: 302, - headers: new Headers([ - ["Location", url], - ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], - ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], - ]), - }); - } catch (err) { - console.error(err); - return Response.redirect(`/?google_login_error`, 302); - } -} - export const onRequestGet: PagesFunction = async ({ request, env }) => { const { CLIENT_ID, @@ -238,8 +25,8 @@ export const onRequestGet: PagesFunction = async ({ request, env }) => { GOOGLE_RESPONSE_TYPE, GOOGLE_SCOPE, } = env; - const reqUrl = new URL(request.url); + // Determine the login provider provider = reqUrl.searchParams.get("provider") ? reqUrl.searchParams.get("provider") : provider; const code = reqUrl.searchParams.get("code"); @@ -249,36 +36,30 @@ export const onRequestGet: PagesFunction = async ({ request, env }) => { const { appUrl, isDev, tokenProvider } = createResourcesForEnv(env.ENVIRONMENT, request.url); if (provider === "google") { - return isDev - ? handleGoogleDevLogin({ - chatId, - JWT_SECRET, - tokenProvider, - appUrl, - }) - : handleGoogleProdLogin({ - code, - chatId, - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - GOOGLE_REDIRECT_URI, - GOOGLE_RESPONSE_TYPE, - GOOGLE_SCOPE, - JWT_SECRET, - tokenProvider, - appUrl, - }); + return handleGoogleLogin({ + // isDev, + isDev: false, + code, + chatId, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + JWT_SECRET, + tokenProvider, + appUrl, + }); } else { - return isDev - ? handleDevLogin({ chatId, JWT_SECRET, tokenProvider, appUrl }) - : handleProdLogin({ - code, - chatId, - CLIENT_ID, - CLIENT_SECRET, - JWT_SECRET, - tokenProvider, - appUrl, - }); + return handleGithubLogin({ + isDev, + code, + chatId, + CLIENT_ID, + CLIENT_SECRET, + JWT_SECRET, + tokenProvider, + appUrl, + }); } }; diff --git a/functions/github.ts b/functions/github.ts index d1bbd129..db06c2a2 100644 --- a/functions/github.ts +++ b/functions/github.ts @@ -1,3 +1,4 @@ +import { TokenProvider } from "./token-provider"; import { buildUrl } from "./utils"; export async function requestAccessToken(code: string, CLIENT_ID: string, CLIENT_SECRET: string) { @@ -62,3 +63,117 @@ export function requestDevUserInfo() { avatarUrl: "https://github.com/github.png?size=402", }; } + +export function handleGithubLogin({ + isDev, + code, + chatId, + CLIENT_ID, + CLIENT_SECRET, + JWT_SECRET, + tokenProvider, + appUrl, +}) { + return isDev + ? handleGithubDevLogin({ chatId, JWT_SECRET, tokenProvider, appUrl }) + : handleGithubProdLogin({ + code, + chatId, + CLIENT_ID, + CLIENT_SECRET, + JWT_SECRET, + tokenProvider, + appUrl, + }); +} +// Authenticate the user with GitHub, then create a JWT for use in ChatCraft. +// We store the token in a secure, HTTP-only cookie. +export async function handleGithubProdLogin({ + code, + chatId, + CLIENT_ID, + CLIENT_SECRET, + JWT_SECRET, + tokenProvider, + appUrl, +}: { + code: string | null; + chatId: string | null; + CLIENT_ID: string; + CLIENT_SECRET: string; + JWT_SECRET: string; + tokenProvider: TokenProvider; + appUrl: string; +}) { + // If we're missing the code, redirect to the GitHub Auth UI + if (!code) { + const url = buildUrl( + "https://github.com/login/oauth/authorize", + // If there's a chatId, piggy-back it on the request as state + chatId ? { client_id: CLIENT_ID, state: chatId } : { client_id: CLIENT_ID } + ); + return Response.redirect(url, 302); + } + + // Otherwise, exchange the code for an access_token, then get user info + // and use that to create JWTs for ChatCraft. + try { + const ghAccessToken = await requestAccessToken(code, CLIENT_ID, CLIENT_SECRET); + const user = await requestUserInfo(ghAccessToken); + // User info goes in a non HTTP-Only cookie that browser can read + const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); + // API authorization goes in an HTTP-Only cookie that only functions can read + const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); + + // Return to the root or a specific chat if we have an id + const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; + + return new Response(null, { + status: 302, + headers: new Headers([ + ["Location", url], + ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], + ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], + ]), + }); + } catch (err) { + console.error(err); + return Response.redirect(`https://chatcraft.org/?github_login_error`, 302); + } +} + +// In development, we simulate a GitHub login. +export async function handleGithubDevLogin({ + chatId, + JWT_SECRET, + tokenProvider, + appUrl, +}: { + chatId: string | null; + JWT_SECRET: string; + tokenProvider: TokenProvider; + appUrl: string; +}) { + try { + const user = requestDevUserInfo(); + // User info goes in a non HTTP-Only cookie that browser can read + const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); + // API authorization goes in an HTTP-Only cookie that only functions can read + const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); + + // Return to the root or a specific chat if we have an id + const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; + + return new Response(null, { + status: 302, + headers: new Headers([ + ["Location", url], + ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], + ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], + ]), + }); + } catch (err) { + console.error(err); + return Response.redirect(`/?github_login_error`, 302); + } +} diff --git a/functions/google.ts b/functions/google.ts index 8bdb05d1..e7eceaaa 100644 --- a/functions/google.ts +++ b/functions/google.ts @@ -1,3 +1,4 @@ +import { TokenProvider } from "./token-provider"; import { buildUrl } from "./utils"; // https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code @@ -68,3 +69,151 @@ export function requestGoogleDevUserInfo() { "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg?size=402", }; } + +export function handleGoogleLogin({ + isDev, + code, + chatId, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + JWT_SECRET, + tokenProvider, + appUrl, +}) { + return isDev + ? handleGoogleDevLogin({ + chatId, + JWT_SECRET, + tokenProvider, + appUrl, + }) + : handleGoogleProdLogin({ + code, + chatId, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + JWT_SECRET, + tokenProvider, + appUrl, + }); +} + +export async function handleGoogleProdLogin({ + code, + chatId, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI, + GOOGLE_RESPONSE_TYPE, + GOOGLE_SCOPE, + JWT_SECRET, + tokenProvider, + appUrl, +}: { + code: string | null; + chatId: string | null; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + GOOGLE_REDIRECT_URI: string; + GOOGLE_RESPONSE_TYPE: string; + GOOGLE_SCOPE: string; + JWT_SECRET: string; + tokenProvider: TokenProvider; + appUrl: string; +}) { + // If we're missing the code, redirect to the Google Auth UI + if (!code) { + const url = buildUrl( + "https://accounts.google.com/o/oauth2/v2/auth", + // If there's a chatId, piggy-back it on the request as state + chatId + ? { + client_id: GOOGLE_CLIENT_ID, + redirect_uri: GOOGLE_REDIRECT_URI, + response_type: GOOGLE_RESPONSE_TYPE, + scope: GOOGLE_SCOPE, + state: chatId, + } + : { + client_id: GOOGLE_CLIENT_ID, + redirect_uri: GOOGLE_REDIRECT_URI, + response_type: GOOGLE_RESPONSE_TYPE, + scope: GOOGLE_SCOPE, + } + ); + return Response.redirect(url, 302); + } + + // Otherwise, exchange the code for an access_token, then get user info + // and use that to create JWTs for ChatCraft. + try { + const googleAccessToken = await requestGoogleAccessToken( + code, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + GOOGLE_REDIRECT_URI + ); + const user = await requestGoogleUserInfo(googleAccessToken); + // User info goes in a non HTTP-Only cookie that browser can read + const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); + // API authorization goes in an HTTP-Only cookie that only functions can read + const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); + + // Return to the root or a specific chat if we have an id + const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; + + return new Response(null, { + status: 302, + headers: new Headers([ + ["Location", url], + ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], + ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], + ]), + }); + } catch (err) { + console.error(err); + return Response.redirect(`https://chatcraft.org/?google_login_error`, 302); + } +} + +// In development, we simulate a Google login. +export async function handleGoogleDevLogin({ + chatId, + JWT_SECRET, + tokenProvider, + appUrl, +}: { + chatId: string | null; + JWT_SECRET: string; + tokenProvider: TokenProvider; + appUrl: string; +}) { + try { + const user = requestGoogleDevUserInfo(); + // User info goes in a non HTTP-Only cookie that browser can read + const idToken = await tokenProvider.createToken(user.username, user, JWT_SECRET); + // API authorization goes in an HTTP-Only cookie that only functions can read + const accessToken = await tokenProvider.createToken(user.username, { role: "api" }, JWT_SECRET); + + // Return to the root or a specific chat if we have an id + const url = new URL(chatId ? `/c/${chatId}` : "/", appUrl).href; + + return new Response(null, { + status: 302, + headers: new Headers([ + ["Location", url], + ["Set-Cookie", tokenProvider.serializeToken("access_token", accessToken)], + ["Set-Cookie", tokenProvider.serializeToken("id_token", idToken)], + ]), + }); + } catch (err) { + console.error(err); + return Response.redirect(`/?google_login_error`, 302); + } +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 37faae3b..336315d7 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -131,7 +131,7 @@ function Header({ chatId, inputPromptRef, searchText, onToggleSidebar }: HeaderP Settings... Default System Prompt... - {user && ( + {user ? ( { handleLoginLogout(""); @@ -139,8 +139,7 @@ function Header({ chatId, inputPromptRef, searchText, onToggleSidebar }: HeaderP > Logout - )} - {!user && ( + ) : ( <> { diff --git a/src/router.tsx b/src/router.tsx index 69468191..f3f3437e 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -25,6 +25,7 @@ export default createBrowserRouter([ } catch (err) { console.warn("Error getting most recent chat", err); } + return redirect("/new"); }, errorElement: , From 84a9e4645131877fec3d7dfa46a023cfaf341cd2 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Wed, 14 Feb 2024 22:56:29 -0500 Subject: [PATCH 08/13] revise github and add google login test --- functions/api/login.test.ts | 646 +++++++++++++++++++++++++++++++++++- functions/api/login.ts | 3 +- 2 files changed, 638 insertions(+), 11 deletions(-) diff --git a/functions/api/login.test.ts b/functions/api/login.test.ts index b33a6a29..eda0ec1c 100644 --- a/functions/api/login.test.ts +++ b/functions/api/login.test.ts @@ -2,15 +2,18 @@ import { describe, test, expect } from "vitest"; import { decodeJwt } from "jose"; import { githubMocks } from "../github.test"; -import { handleProdLogin, handleDevLogin } from "./login"; +// import { handleProdLogin, handleDevLogin } from "./login"; +import { handleGithubLogin } from "../github"; +import { handleGoogleLogin } from "../google"; import { TokenProvider } from "../token-provider"; -describe("Production /api/login", () => { +describe("Production Github /api/login", () => { const tokenProvider = new TokenProvider("production", "https://chatcraft.org"); const appUrl = "https://chatcraft.org/"; test("/api/login without code should redirect to GitHub's OAuth login", async () => { - const res = await handleProdLogin({ + const res = await handleGithubLogin({ + isDev: false, code: null, chatId: null, CLIENT_ID: "client_id_1234", @@ -27,7 +30,8 @@ describe("Production /api/login", () => { }); test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { - const res = await handleProdLogin({ + const res = await handleGithubLogin({ + isDev: false, code: null, chatId: "123456", CLIENT_ID: "client_id_1234", @@ -51,7 +55,8 @@ describe("Production /api/login", () => { const mocks = githubMocks(); mocks.all(); - const res = await handleProdLogin({ + const res = await handleGithubLogin({ + isDev: false, code: "ghcode", chatId: null, CLIENT_ID: "client_id_1234", @@ -110,7 +115,8 @@ describe("Production /api/login", () => { const mocks = githubMocks(); mocks.all(); - const res = await handleProdLogin({ + const res = await handleGithubLogin({ + isDev: false, code: "ghcode", chatId: "123456", CLIENT_ID: "client_id_1234", @@ -125,7 +131,7 @@ describe("Production /api/login", () => { }); }); -describe("Development /api/login", () => { +describe("Development Github /api/login", () => { const tokenProvider = new TokenProvider("development", "http://localhost:9339"); const appUrl = "http://localhost:9339/"; @@ -134,8 +140,626 @@ describe("Development /api/login", () => { const mocks = githubMocks(); mocks.all(); - const res = await handleDevLogin({ + const res = await handleGithubLogin({ + isDev: true, + code: null, + chatId: null, + CLIENT_ID: null, + CLIENT_SECRET: null, + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("http://localhost:9339/"); + + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== "set-cookie") { + return; + } + + if (value.startsWith("access_token")) { + expect(value).toMatch( + /access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; SameSite=Strict/ + ); + const matches = value.match(/access_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const accessToken = matches && matches[1]; + if (!accessToken) { + expect.fail("missing access token"); + } else { + const payload = decodeJwt(accessToken); + expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.role).toEqual("api"); + } + } else { + expect(value).toMatch(/id_token=[^;]+; Max-Age=2592000; Path=\/; SameSite=Strict/); + const matches = value.match(/id_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const idToken = matches && matches[1]; + if (!idToken) { + expect.fail("missing id token"); + } else { + const payload = decodeJwt(idToken); + expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.username).toEqual("chatcraft_dev"); + expect(payload.name).toEqual("ChatCraftDev"); + expect(payload.avatarUrl).toEqual("https://github.com/github.png?size=402"); + } + } + }); + }); + + test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: true, + code: null, + chatId: "123456", + CLIENT_ID: null, + CLIENT_SECRET: null, + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("http://localhost:9339/c/123456"); + }); + + test("/api/login without code should redirect to GitHub's OAuth login", async () => { + const res = await handleGithubLogin({ + isDev: false, + code: null, + chatId: null, + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual( + "https://github.com/login/oauth/authorize?client_id=client_id_1234" + ); + }); + + test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { + const res = await handleGithubLogin({ + isDev: false, + code: null, + chatId: "123456", + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + const location = res.headers.get("Location"); + expect(typeof location).toBe("string"); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const params = new URL(location!).searchParams; + expect(params.get("client_id")).toEqual("client_id_1234"); + expect(params.get("state")).toEqual("123456"); + }); + + test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: false, + code: "ghcode", + chatId: null, + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("https://chatcraft.org/"); + + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== "set-cookie") { + return; + } + + if (value.startsWith("__Host-access_token")) { + expect(value).toMatch( + /__Host-access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; Secure; SameSite=Strict/ + ); + const matches = value.match(/__Host-access_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const accessToken = matches && matches[1]; + if (!accessToken) { + expect.fail("missing access token"); + } else { + const payload = decodeJwt(accessToken); + expect(payload.sub).toEqual("login"); + expect(payload.role).toEqual("api"); + } + } else { + expect(value).toMatch( + /__Host-id_token=[^;]+; Max-Age=2592000; Path=\/; Secure; SameSite=Strict/ + ); + const matches = value.match(/__Host-id_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const idToken = matches && matches[1]; + if (!idToken) { + expect.fail("missing id token"); + } else { + const payload = decodeJwt(idToken); + expect(payload.sub).toEqual("login"); + expect(payload.username).toEqual("login"); + expect(payload.name).toEqual("name"); + expect(payload.avatarUrl).toEqual("avatar_url"); + } + } + }); + }); + + test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: false, + code: "ghcode", + chatId: "123456", + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("https://chatcraft.org/c/123456"); + }); +}); + +describe("Development Github /api/login", () => { + const tokenProvider = new TokenProvider("development", "http://localhost:9339"); + const appUrl = "http://localhost:9339/"; + + test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: true, + code: null, + chatId: null, + CLIENT_ID: null, + CLIENT_SECRET: null, + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("http://localhost:9339/"); + + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== "set-cookie") { + return; + } + + if (value.startsWith("access_token")) { + expect(value).toMatch( + /access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; SameSite=Strict/ + ); + const matches = value.match(/access_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const accessToken = matches && matches[1]; + if (!accessToken) { + expect.fail("missing access token"); + } else { + const payload = decodeJwt(accessToken); + expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.role).toEqual("api"); + } + } else { + expect(value).toMatch(/id_token=[^;]+; Max-Age=2592000; Path=\/; SameSite=Strict/); + const matches = value.match(/id_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const idToken = matches && matches[1]; + if (!idToken) { + expect.fail("missing id token"); + } else { + const payload = decodeJwt(idToken); + expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.username).toEqual("chatcraft_dev"); + expect(payload.name).toEqual("ChatCraftDev"); + expect(payload.avatarUrl).toEqual("https://github.com/github.png?size=402"); + } + } + }); + }); + + test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: true, + code: null, + chatId: "123456", + CLIENT_ID: null, + CLIENT_SECRET: null, + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("http://localhost:9339/c/123456"); + }); +}); + +describe("Production Google /api/login", () => { + const tokenProvider = new TokenProvider("production", "https://chatcraft.org"); + const appUrl = "https://chatcraft.org/"; + + test("/api/login without code should redirect to Google's OAuth login", async () => { + const res = await handleGoogleLogin({ + isDev: false, + code: null, + chatId: null, + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual( + "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email" + ); + }); + + test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { + const res = await handleGithubLogin({ + isDev: false, + code: null, + chatId: "123456", + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + const location = res.headers.get("Location"); + expect(typeof location).toBe("string"); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const params = new URL(location!).searchParams; + expect(params.get("client_id")).toEqual("client_id_1234"); + expect(params.get("state")).toEqual("123456"); + }); + + test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: false, + code: "ghcode", + chatId: null, + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("https://chatcraft.org/"); + + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== "set-cookie") { + return; + } + + if (value.startsWith("__Host-access_token")) { + expect(value).toMatch( + /__Host-access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; Secure; SameSite=Strict/ + ); + const matches = value.match(/__Host-access_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const accessToken = matches && matches[1]; + if (!accessToken) { + expect.fail("missing access token"); + } else { + const payload = decodeJwt(accessToken); + expect(payload.sub).toEqual("login"); + expect(payload.role).toEqual("api"); + } + } else { + expect(value).toMatch( + /__Host-id_token=[^;]+; Max-Age=2592000; Path=\/; Secure; SameSite=Strict/ + ); + const matches = value.match(/__Host-id_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const idToken = matches && matches[1]; + if (!idToken) { + expect.fail("missing id token"); + } else { + const payload = decodeJwt(idToken); + expect(payload.sub).toEqual("login"); + expect(payload.username).toEqual("login"); + expect(payload.name).toEqual("name"); + expect(payload.avatarUrl).toEqual("avatar_url"); + } + } + }); + }); + + test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: false, + code: "ghcode", + chatId: "123456", + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("https://chatcraft.org/c/123456"); + }); +}); + +describe("Development Google /api/login", () => { + const tokenProvider = new TokenProvider("development", "http://localhost:9339"); + const appUrl = "http://localhost:9339/"; + + test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: true, + code: null, + chatId: null, + CLIENT_ID: null, + CLIENT_SECRET: null, + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("http://localhost:9339/"); + + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== "set-cookie") { + return; + } + + if (value.startsWith("access_token")) { + expect(value).toMatch( + /access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; SameSite=Strict/ + ); + const matches = value.match(/access_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const accessToken = matches && matches[1]; + if (!accessToken) { + expect.fail("missing access token"); + } else { + const payload = decodeJwt(accessToken); + expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.role).toEqual("api"); + } + } else { + expect(value).toMatch(/id_token=[^;]+; Max-Age=2592000; Path=\/; SameSite=Strict/); + const matches = value.match(/id_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const idToken = matches && matches[1]; + if (!idToken) { + expect.fail("missing id token"); + } else { + const payload = decodeJwt(idToken); + expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.username).toEqual("chatcraft_dev"); + expect(payload.name).toEqual("ChatCraftDev"); + expect(payload.avatarUrl).toEqual("https://github.com/github.png?size=402"); + } + } + }); + }); + + test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: true, + code: null, + chatId: "123456", + CLIENT_ID: null, + CLIENT_SECRET: null, + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("http://localhost:9339/c/123456"); + }); + + test("/api/login without code should redirect to GitHub's OAuth login", async () => { + const res = await handleGithubLogin({ + isDev: false, + code: null, + chatId: null, + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual( + "https://github.com/login/oauth/authorize?client_id=client_id_1234" + ); + }); + + test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { + const res = await handleGithubLogin({ + isDev: false, + code: null, + chatId: "123456", + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + const location = res.headers.get("Location"); + expect(typeof location).toBe("string"); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const params = new URL(location!).searchParams; + expect(params.get("client_id")).toEqual("client_id_1234"); + expect(params.get("state")).toEqual("123456"); + }); + + test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: false, + code: "ghcode", + chatId: null, + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("https://chatcraft.org/"); + + res.headers.forEach((value, key) => { + if (key.toLowerCase() !== "set-cookie") { + return; + } + + if (value.startsWith("__Host-access_token")) { + expect(value).toMatch( + /__Host-access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; Secure; SameSite=Strict/ + ); + const matches = value.match(/__Host-access_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const accessToken = matches && matches[1]; + if (!accessToken) { + expect.fail("missing access token"); + } else { + const payload = decodeJwt(accessToken); + expect(payload.sub).toEqual("login"); + expect(payload.role).toEqual("api"); + } + } else { + expect(value).toMatch( + /__Host-id_token=[^;]+; Max-Age=2592000; Path=\/; Secure; SameSite=Strict/ + ); + const matches = value.match(/__Host-id_token=([^;]+);/); + expect(Array.isArray(matches)).toBe(true); + expect(matches?.length).toBe(2); + const idToken = matches && matches[1]; + if (!idToken) { + expect.fail("missing id token"); + } else { + const payload = decodeJwt(idToken); + expect(payload.sub).toEqual("login"); + expect(payload.username).toEqual("login"); + expect(payload.name).toEqual("name"); + expect(payload.avatarUrl).toEqual("avatar_url"); + } + } + }); + }); + + test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: false, + code: "ghcode", + chatId: "123456", + CLIENT_ID: "client_id_1234", + CLIENT_SECRET: "client_secret", + JWT_SECRET: "jwt_secret", + tokenProvider, + appUrl, + }); + + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toEqual("https://chatcraft.org/c/123456"); + }); +}); + +describe("Development Google /api/login", () => { + const tokenProvider = new TokenProvider("development", "http://localhost:9339"); + const appUrl = "http://localhost:9339/"; + + test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { + // Mock GitHub OAuth and /user flow + const mocks = githubMocks(); + mocks.all(); + + const res = await handleGithubLogin({ + isDev: true, + code: null, chatId: null, + CLIENT_ID: null, + CLIENT_SECRET: null, JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -188,8 +812,12 @@ describe("Development /api/login", () => { const mocks = githubMocks(); mocks.all(); - const res = await handleDevLogin({ + const res = await handleGithubLogin({ + isDev: true, + code: null, chatId: "123456", + CLIENT_ID: null, + CLIENT_SECRET: null, JWT_SECRET: "jwt_secret", tokenProvider, appUrl, diff --git a/functions/api/login.ts b/functions/api/login.ts index 83d66cfe..9588c538 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -37,8 +37,7 @@ export const onRequestGet: PagesFunction = async ({ request, env }) => { if (provider === "google") { return handleGoogleLogin({ - // isDev, - isDev: false, + isDev, code, chatId, GOOGLE_CLIENT_ID, From 5db164591d19056104bdc7f5a27e222325de8c0d Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Wed, 14 Feb 2024 23:38:41 -0500 Subject: [PATCH 09/13] revise login test --- functions/api/login.test.ts | 171 ++++++++++++++++++++++-------------- 1 file changed, 104 insertions(+), 67 deletions(-) diff --git a/functions/api/login.test.ts b/functions/api/login.test.ts index eda0ec1c..1111b0c2 100644 --- a/functions/api/login.test.ts +++ b/functions/api/login.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest"; import { decodeJwt } from "jose"; import { githubMocks } from "../github.test"; -// import { handleProdLogin, handleDevLogin } from "./login"; +import { googleMocks } from "../google.test"; import { handleGithubLogin } from "../github"; import { handleGoogleLogin } from "../google"; import { TokenProvider } from "../token-provider"; @@ -437,17 +437,20 @@ describe("Production Google /api/login", () => { expect(res.status).toBe(302); expect(res.headers.get("Location")).toEqual( - "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email" + "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&redirect_uri=https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email" ); }); test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: false, code: null, chatId: "123456", - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -463,16 +466,19 @@ describe("Production Google /api/login", () => { }); test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + // Mock Google OAuth and /user flow + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: false, - code: "ghcode", + code: "gcode", chatId: null, - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -498,7 +504,7 @@ describe("Production Google /api/login", () => { expect.fail("missing access token"); } else { const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("login"); + expect(payload.sub).toEqual("email"); expect(payload.role).toEqual("api"); } } else { @@ -513,10 +519,10 @@ describe("Production Google /api/login", () => { expect.fail("missing id token"); } else { const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("login"); - expect(payload.username).toEqual("login"); + expect(payload.sub).toEqual("email"); + expect(payload.username).toEqual("email"); expect(payload.name).toEqual("name"); - expect(payload.avatarUrl).toEqual("avatar_url"); + expect(payload.avatarUrl).toEqual("picture"); } } }); @@ -524,15 +530,18 @@ describe("Production Google /api/login", () => { test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: false, - code: "ghcode", + code: "gcode", chatId: "123456", - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -548,16 +557,19 @@ describe("Development Google /api/login", () => { const appUrl = "http://localhost:9339/"; test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + // Mock Google OAuth and /user flow + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: true, code: null, chatId: null, - CLIENT_ID: null, - CLIENT_SECRET: null, + GOOGLE_CLIENT_ID: null, + GOOGLE_CLIENT_SECRET: null, + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -583,7 +595,7 @@ describe("Development Google /api/login", () => { expect.fail("missing access token"); } else { const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.sub).toEqual("chatcraft_dev_google"); expect(payload.role).toEqual("api"); } } else { @@ -596,10 +608,12 @@ describe("Development Google /api/login", () => { expect.fail("missing id token"); } else { const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("chatcraft_dev"); - expect(payload.username).toEqual("chatcraft_dev"); - expect(payload.name).toEqual("ChatCraftDev"); - expect(payload.avatarUrl).toEqual("https://github.com/github.png?size=402"); + expect(payload.sub).toEqual("chatcraft_dev_google"); + expect(payload.username).toEqual("chatcraft_dev_google"); + expect(payload.name).toEqual("ChatCraftDevGoogle"); + expect(payload.avatarUrl).toEqual( + "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg?size=402" + ); } } }); @@ -607,15 +621,18 @@ describe("Development Google /api/login", () => { test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: true, code: null, chatId: "123456", - CLIENT_ID: null, - CLIENT_SECRET: null, + GOOGLE_CLIENT_ID: null, + GOOGLE_CLIENT_SECRET: null, + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -626,12 +643,15 @@ describe("Development Google /api/login", () => { }); test("/api/login without code should redirect to GitHub's OAuth login", async () => { - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: false, code: null, chatId: null, - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -639,17 +659,20 @@ describe("Development Google /api/login", () => { expect(res.status).toBe(302); expect(res.headers.get("Location")).toEqual( - "https://github.com/login/oauth/authorize?client_id=client_id_1234" + "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&redirect_uri=https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email" ); }); - test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { - const res = await handleGithubLogin({ + test("/api/login without code and with chatId should redirect to Google's OAuth login with state", async () => { + const res = await handleGoogleLogin({ isDev: false, code: null, chatId: "123456", - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -666,15 +689,18 @@ describe("Development Google /api/login", () => { test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: false, - code: "ghcode", + code: "gcode", chatId: null, - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -725,16 +751,19 @@ describe("Development Google /api/login", () => { }); test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + // Mock Google OAuth and /user flow + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: false, code: "ghcode", chatId: "123456", - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", + GOOGLE_CLIENT_ID: "client_id_1234", + GOOGLE_CLIENT_SECRET: "client_secret", + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -750,16 +779,19 @@ describe("Development Google /api/login", () => { const appUrl = "http://localhost:9339/"; test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + // Mock Google OAuth and /user flow + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: true, code: null, chatId: null, - CLIENT_ID: null, - CLIENT_SECRET: null, + GOOGLE_CLIENT_ID: null, + GOOGLE_CLIENT_SECRET: null, + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, @@ -785,7 +817,7 @@ describe("Development Google /api/login", () => { expect.fail("missing access token"); } else { const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("chatcraft_dev"); + expect(payload.sub).toEqual("chatcraft_dev_google"); expect(payload.role).toEqual("api"); } } else { @@ -798,26 +830,31 @@ describe("Development Google /api/login", () => { expect.fail("missing id token"); } else { const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("chatcraft_dev"); - expect(payload.username).toEqual("chatcraft_dev"); - expect(payload.name).toEqual("ChatCraftDev"); - expect(payload.avatarUrl).toEqual("https://github.com/github.png?size=402"); + expect(payload.sub).toEqual("chatcraft_dev_google"); + expect(payload.username).toEqual("chatcraft_dev_google"); + expect(payload.name).toEqual("ChatCraftDevGoogle"); + expect(payload.avatarUrl).toEqual( + "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg?size=402" + ); } } }); }); test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); + // Mock Google OAuth and /user flow + const mocks = googleMocks(); mocks.all(); - const res = await handleGithubLogin({ + const res = await handleGoogleLogin({ isDev: true, code: null, chatId: "123456", - CLIENT_ID: null, - CLIENT_SECRET: null, + GOOGLE_CLIENT_ID: null, + GOOGLE_CLIENT_SECRET: null, + GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", + GOOGLE_RESPONSE_TYPE: "code", + GOOGLE_SCOPE: "profile email", JWT_SECRET: "jwt_secret", tokenProvider, appUrl, From f00332588c807037ed1be8dbdd4e26724377e1d3 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Thu, 15 Feb 2024 00:01:28 -0500 Subject: [PATCH 10/13] revise login test --- functions/api/login.test.ts | 426 +----------------------------------- 1 file changed, 1 insertion(+), 425 deletions(-) diff --git a/functions/api/login.test.ts b/functions/api/login.test.ts index 1111b0c2..6109a269 100644 --- a/functions/api/login.test.ts +++ b/functions/api/login.test.ts @@ -131,208 +131,6 @@ describe("Production Github /api/login", () => { }); }); -describe("Development Github /api/login", () => { - const tokenProvider = new TokenProvider("development", "http://localhost:9339"); - const appUrl = "http://localhost:9339/"; - - test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); - mocks.all(); - - const res = await handleGithubLogin({ - isDev: true, - code: null, - chatId: null, - CLIENT_ID: null, - CLIENT_SECRET: null, - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("http://localhost:9339/"); - - res.headers.forEach((value, key) => { - if (key.toLowerCase() !== "set-cookie") { - return; - } - - if (value.startsWith("access_token")) { - expect(value).toMatch( - /access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; SameSite=Strict/ - ); - const matches = value.match(/access_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const accessToken = matches && matches[1]; - if (!accessToken) { - expect.fail("missing access token"); - } else { - const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("chatcraft_dev"); - expect(payload.role).toEqual("api"); - } - } else { - expect(value).toMatch(/id_token=[^;]+; Max-Age=2592000; Path=\/; SameSite=Strict/); - const matches = value.match(/id_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const idToken = matches && matches[1]; - if (!idToken) { - expect.fail("missing id token"); - } else { - const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("chatcraft_dev"); - expect(payload.username).toEqual("chatcraft_dev"); - expect(payload.name).toEqual("ChatCraftDev"); - expect(payload.avatarUrl).toEqual("https://github.com/github.png?size=402"); - } - } - }); - }); - - test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); - mocks.all(); - - const res = await handleGithubLogin({ - isDev: true, - code: null, - chatId: "123456", - CLIENT_ID: null, - CLIENT_SECRET: null, - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("http://localhost:9339/c/123456"); - }); - - test("/api/login without code should redirect to GitHub's OAuth login", async () => { - const res = await handleGithubLogin({ - isDev: false, - code: null, - chatId: null, - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual( - "https://github.com/login/oauth/authorize?client_id=client_id_1234" - ); - }); - - test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { - const res = await handleGithubLogin({ - isDev: false, - code: null, - chatId: "123456", - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - const location = res.headers.get("Location"); - expect(typeof location).toBe("string"); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const params = new URL(location!).searchParams; - expect(params.get("client_id")).toEqual("client_id_1234"); - expect(params.get("state")).toEqual("123456"); - }); - - test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); - mocks.all(); - - const res = await handleGithubLogin({ - isDev: false, - code: "ghcode", - chatId: null, - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("https://chatcraft.org/"); - - res.headers.forEach((value, key) => { - if (key.toLowerCase() !== "set-cookie") { - return; - } - - if (value.startsWith("__Host-access_token")) { - expect(value).toMatch( - /__Host-access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; Secure; SameSite=Strict/ - ); - const matches = value.match(/__Host-access_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const accessToken = matches && matches[1]; - if (!accessToken) { - expect.fail("missing access token"); - } else { - const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("login"); - expect(payload.role).toEqual("api"); - } - } else { - expect(value).toMatch( - /__Host-id_token=[^;]+; Max-Age=2592000; Path=\/; Secure; SameSite=Strict/ - ); - const matches = value.match(/__Host-id_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const idToken = matches && matches[1]; - if (!idToken) { - expect.fail("missing id token"); - } else { - const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("login"); - expect(payload.username).toEqual("login"); - expect(payload.name).toEqual("name"); - expect(payload.avatarUrl).toEqual("avatar_url"); - } - } - }); - }); - - test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock GitHub OAuth and /user flow - const mocks = githubMocks(); - mocks.all(); - - const res = await handleGithubLogin({ - isDev: false, - code: "ghcode", - chatId: "123456", - CLIENT_ID: "client_id_1234", - CLIENT_SECRET: "client_secret", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("https://chatcraft.org/c/123456"); - }); -}); - describe("Development Github /api/login", () => { const tokenProvider = new TokenProvider("development", "http://localhost:9339"); const appUrl = "http://localhost:9339/"; @@ -441,7 +239,7 @@ describe("Production Google /api/login", () => { ); }); - test("/api/login without code and with chatId should redirect to GitHub's OAuth login with state", async () => { + test("/api/login without code and with chatId should redirect to Google's OAuth login with state", async () => { const res = await handleGoogleLogin({ isDev: false, code: null, @@ -529,235 +327,13 @@ describe("Production Google /api/login", () => { }); test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock GitHub OAuth and /user flow - const mocks = googleMocks(); - mocks.all(); - - const res = await handleGoogleLogin({ - isDev: false, - code: "gcode", - chatId: "123456", - GOOGLE_CLIENT_ID: "client_id_1234", - GOOGLE_CLIENT_SECRET: "client_secret", - GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", - GOOGLE_RESPONSE_TYPE: "code", - GOOGLE_SCOPE: "profile email", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("https://chatcraft.org/c/123456"); - }); -}); - -describe("Development Google /api/login", () => { - const tokenProvider = new TokenProvider("development", "http://localhost:9339"); - const appUrl = "http://localhost:9339/"; - - test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { // Mock Google OAuth and /user flow const mocks = googleMocks(); mocks.all(); - const res = await handleGoogleLogin({ - isDev: true, - code: null, - chatId: null, - GOOGLE_CLIENT_ID: null, - GOOGLE_CLIENT_SECRET: null, - GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", - GOOGLE_RESPONSE_TYPE: "code", - GOOGLE_SCOPE: "profile email", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("http://localhost:9339/"); - - res.headers.forEach((value, key) => { - if (key.toLowerCase() !== "set-cookie") { - return; - } - - if (value.startsWith("access_token")) { - expect(value).toMatch( - /access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; SameSite=Strict/ - ); - const matches = value.match(/access_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const accessToken = matches && matches[1]; - if (!accessToken) { - expect.fail("missing access token"); - } else { - const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("chatcraft_dev_google"); - expect(payload.role).toEqual("api"); - } - } else { - expect(value).toMatch(/id_token=[^;]+; Max-Age=2592000; Path=\/; SameSite=Strict/); - const matches = value.match(/id_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const idToken = matches && matches[1]; - if (!idToken) { - expect.fail("missing id token"); - } else { - const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("chatcraft_dev_google"); - expect(payload.username).toEqual("chatcraft_dev_google"); - expect(payload.name).toEqual("ChatCraftDevGoogle"); - expect(payload.avatarUrl).toEqual( - "https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg?size=402" - ); - } - } - }); - }); - - test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock GitHub OAuth and /user flow - const mocks = googleMocks(); - mocks.all(); - - const res = await handleGoogleLogin({ - isDev: true, - code: null, - chatId: "123456", - GOOGLE_CLIENT_ID: null, - GOOGLE_CLIENT_SECRET: null, - GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", - GOOGLE_RESPONSE_TYPE: "code", - GOOGLE_SCOPE: "profile email", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("http://localhost:9339/c/123456"); - }); - - test("/api/login without code should redirect to GitHub's OAuth login", async () => { - const res = await handleGoogleLogin({ - isDev: false, - code: null, - chatId: null, - GOOGLE_CLIENT_ID: "client_id_1234", - GOOGLE_CLIENT_SECRET: "client_secret", - GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", - GOOGLE_RESPONSE_TYPE: "code", - GOOGLE_SCOPE: "profile email", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual( - "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&redirect_uri=https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email" - ); - }); - - test("/api/login without code and with chatId should redirect to Google's OAuth login with state", async () => { - const res = await handleGoogleLogin({ - isDev: false, - code: null, - chatId: "123456", - GOOGLE_CLIENT_ID: "client_id_1234", - GOOGLE_CLIENT_SECRET: "client_secret", - GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", - GOOGLE_RESPONSE_TYPE: "code", - GOOGLE_SCOPE: "profile email", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - const location = res.headers.get("Location"); - expect(typeof location).toBe("string"); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const params = new URL(location!).searchParams; - expect(params.get("client_id")).toEqual("client_id_1234"); - expect(params.get("state")).toEqual("123456"); - }); - - test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { - // Mock GitHub OAuth and /user flow - const mocks = googleMocks(); - mocks.all(); - const res = await handleGoogleLogin({ isDev: false, code: "gcode", - chatId: null, - GOOGLE_CLIENT_ID: "client_id_1234", - GOOGLE_CLIENT_SECRET: "client_secret", - GOOGLE_REDIRECT_URI: "https://chatcraft.org/api/login/", - GOOGLE_RESPONSE_TYPE: "code", - GOOGLE_SCOPE: "profile email", - JWT_SECRET: "jwt_secret", - tokenProvider, - appUrl, - }); - - expect(res.status).toBe(302); - expect(res.headers.get("Location")).toEqual("https://chatcraft.org/"); - - res.headers.forEach((value, key) => { - if (key.toLowerCase() !== "set-cookie") { - return; - } - - if (value.startsWith("__Host-access_token")) { - expect(value).toMatch( - /__Host-access_token=[^;]+; Max-Age=2592000; Path=\/; HttpOnly; Secure; SameSite=Strict/ - ); - const matches = value.match(/__Host-access_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const accessToken = matches && matches[1]; - if (!accessToken) { - expect.fail("missing access token"); - } else { - const payload = decodeJwt(accessToken); - expect(payload.sub).toEqual("login"); - expect(payload.role).toEqual("api"); - } - } else { - expect(value).toMatch( - /__Host-id_token=[^;]+; Max-Age=2592000; Path=\/; Secure; SameSite=Strict/ - ); - const matches = value.match(/__Host-id_token=([^;]+);/); - expect(Array.isArray(matches)).toBe(true); - expect(matches?.length).toBe(2); - const idToken = matches && matches[1]; - if (!idToken) { - expect.fail("missing id token"); - } else { - const payload = decodeJwt(idToken); - expect(payload.sub).toEqual("login"); - expect(payload.username).toEqual("login"); - expect(payload.name).toEqual("name"); - expect(payload.avatarUrl).toEqual("avatar_url"); - } - } - }); - }); - - test("/api/login with code and state should redirect to ChatCraft.org/c/:chatId", async () => { - // Mock Google OAuth and /user flow - const mocks = googleMocks(); - mocks.all(); - - const res = await handleGoogleLogin({ - isDev: false, - code: "ghcode", chatId: "123456", GOOGLE_CLIENT_ID: "client_id_1234", GOOGLE_CLIENT_SECRET: "client_secret", From e682caa90ef307faf2055482c155809ab4d46c7b Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Thu, 15 Feb 2024 11:12:28 -0500 Subject: [PATCH 11/13] get provider from origin auth page --- functions/api/login.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/functions/api/login.ts b/functions/api/login.ts index 9588c538..e9448cc9 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -13,7 +13,6 @@ interface Env { GOOGLE_SCOPE: string; } -let provider: string | null = ""; export const onRequestGet: PagesFunction = async ({ request, env }) => { const { CLIENT_ID, @@ -28,7 +27,11 @@ export const onRequestGet: PagesFunction = async ({ request, env }) => { const reqUrl = new URL(request.url); // Determine the login provider - provider = reqUrl.searchParams.get("provider") ? reqUrl.searchParams.get("provider") : provider; + const provider = reqUrl.searchParams.get("provider") + ? reqUrl.searchParams.get("provider") + : reqUrl.origin.includes("github") + ? "github" + : "google"; const code = reqUrl.searchParams.get("code"); // Include ?chat_id=... to redirect back to a given chat in the client. GitHub will // return it back to us via ?state=... From 9b1569cfcabc0cbae89a8c4834bf802bd89885af Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Thu, 15 Feb 2024 13:25:10 -0500 Subject: [PATCH 12/13] add provider in state --- functions/api/login.test.ts | 8 ++++---- functions/api/login.ts | 31 +++++++++++++++++++++++-------- functions/github.ts | 4 +++- functions/google.ts | 3 ++- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/functions/api/login.test.ts b/functions/api/login.test.ts index 6109a269..06c8d48e 100644 --- a/functions/api/login.test.ts +++ b/functions/api/login.test.ts @@ -25,7 +25,7 @@ describe("Production Github /api/login", () => { expect(res.status).toBe(302); expect(res.headers.get("Location")).toEqual( - "https://github.com/login/oauth/authorize?client_id=client_id_1234" + "https://github.com/login/oauth/authorize?client_id=client_id_1234&state=provider%3Dgithub" ); }); @@ -47,7 +47,7 @@ describe("Production Github /api/login", () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const params = new URL(location!).searchParams; expect(params.get("client_id")).toEqual("client_id_1234"); - expect(params.get("state")).toEqual("123456"); + expect(params.get("state")).toEqual("provider=google&chat_id=123456"); }); test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { @@ -235,7 +235,7 @@ describe("Production Google /api/login", () => { expect(res.status).toBe(302); expect(res.headers.get("Location")).toEqual( - "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&redirect_uri=https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email" + "https://accounts.google.com/o/oauth2/v2/auth?client_id=client_id_1234&redirect_uri=https%3A%2F%2Fchatcraft.org%2Fapi%2Flogin%2F&response_type=code&scope=profile+email&state=provider%3Dgoogle" ); }); @@ -260,7 +260,7 @@ describe("Production Google /api/login", () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const params = new URL(location!).searchParams; expect(params.get("client_id")).toEqual("client_id_1234"); - expect(params.get("state")).toEqual("123456"); + expect(params.get("state")).toEqual("provider=google&chat_id=123456"); }); test("/api/login with code should redirect to ChatCraft.org with cookies", async () => { diff --git a/functions/api/login.ts b/functions/api/login.ts index e9448cc9..939ea788 100644 --- a/functions/api/login.ts +++ b/functions/api/login.ts @@ -27,15 +27,30 @@ export const onRequestGet: PagesFunction = async ({ request, env }) => { const reqUrl = new URL(request.url); // Determine the login provider - const provider = reqUrl.searchParams.get("provider") - ? reqUrl.searchParams.get("provider") - : reqUrl.origin.includes("github") - ? "github" - : "google"; + let provider = reqUrl.searchParams.get("provider"); + if (!provider) { + let state = reqUrl.searchParams.get("state"); + if (state) { + state = decodeURIComponent(state); + const stateParams = new URLSearchParams(state); + provider = stateParams.get("provider"); + } + } + + // Include ?chat_id=... to redirect back to a given chat in the client. Google will + // return it back to us via ?state=provider%3Dgoogle%26chat_id%3Dl77... + let chatId = reqUrl.searchParams.get("chat_id"); + if (!chatId) { + let state = reqUrl.searchParams.get("state"); + if (state) { + state = decodeURIComponent(state); + const stateParams = new URLSearchParams(state); + chatId = stateParams.get("chatId"); + } + } + const code = reqUrl.searchParams.get("code"); - // Include ?chat_id=... to redirect back to a given chat in the client. GitHub will - // return it back to us via ?state=... - const chatId = reqUrl.searchParams.get("chat_id") || reqUrl.searchParams.get("state"); + const { appUrl, isDev, tokenProvider } = createResourcesForEnv(env.ENVIRONMENT, request.url); if (provider === "google") { diff --git a/functions/github.ts b/functions/github.ts index db06c2a2..aa1b92ac 100644 --- a/functions/github.ts +++ b/functions/github.ts @@ -110,7 +110,9 @@ export async function handleGithubProdLogin({ const url = buildUrl( "https://github.com/login/oauth/authorize", // If there's a chatId, piggy-back it on the request as state - chatId ? { client_id: CLIENT_ID, state: chatId } : { client_id: CLIENT_ID } + chatId + ? { client_id: CLIENT_ID, state: "provider=google&chat_id=" + chatId } + : { client_id: CLIENT_ID, state: "provider=github" } ); return Response.redirect(url, 302); } diff --git a/functions/google.ts b/functions/google.ts index e7eceaaa..0a37d042 100644 --- a/functions/google.ts +++ b/functions/google.ts @@ -138,13 +138,14 @@ export async function handleGoogleProdLogin({ redirect_uri: GOOGLE_REDIRECT_URI, response_type: GOOGLE_RESPONSE_TYPE, scope: GOOGLE_SCOPE, - state: chatId, + state: "provider=google&chat_id=" + chatId, } : { client_id: GOOGLE_CLIENT_ID, redirect_uri: GOOGLE_REDIRECT_URI, response_type: GOOGLE_RESPONSE_TYPE, scope: GOOGLE_SCOPE, + state: "provider=google", } ); return Response.redirect(url, 302); From 5a825df6d065cdbf4e893f894c7bd3a749a75e75 Mon Sep 17 00:00:00 2001 From: YumeiWang Date: Thu, 15 Feb 2024 18:24:26 -0500 Subject: [PATCH 13/13] fix typo --- functions/google.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/google.ts b/functions/google.ts index 0a37d042..f78cfd96 100644 --- a/functions/google.ts +++ b/functions/google.ts @@ -60,7 +60,7 @@ export async function requestGoogleUserInfo(token: string): Promise { return { username: email, name: name, avatarUrl: picture }; } -// In development environments, we automatically log the user in without involving GitHub +// In development environments, we automatically log the user in without involving Google export function requestGoogleDevUserInfo() { return { username: "chatcraft_dev_google",