Skip to content

Instantly share code, notes, and snippets.

@yashomi-t3h
Last active January 31, 2025 15:48
Show Gist options
  • Save yashomi-t3h/79f738a1006b4b8b0d73dd354471f83e to your computer and use it in GitHub Desktop.
Save yashomi-t3h/79f738a1006b4b8b0d73dd354471f83e to your computer and use it in GitHub Desktop.
Authenticating on chrome extension with OpenSaas/wasp-lang backend
//extn/background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "LOGIN") {
saveUser().then((response) => {
console.log("Login response: ", response);
chrome.storage.sync.set({ token: response.token }, function () {
console.log('Token is set to ' + response.token);
console.log('User is set to ' + response.user);
chrome.storage.sync.set({ user: JSON.stringify(response.user) }, function () {
console.log('User is set to ' + response.user);
});
sendResponse(response);
});
});
return true; // Keep the message channel open for sendResponse
}
});
import { AuthForExtension } from "wasp/server/api"
import axios from "axios";
import { createUser, findAuthIdentity, ProviderName } from "wasp/auth/utils";
import { tokenStore } from "wasp/server/auth";
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
throw new Error("GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in .env.server file")
}
const clientId = process.env.GOOGLE_CLIENT_ID
const clientSecret = process.env.GOOGLE_CLIENT_SECRET
const redirectURI = "https://<chromeExtensionId>.chromiumapp.org/"
interface GoogleUserResult {
sub: string;
name: string;
given_name: boolean;
family_name: string;
picture: string;
email: string;
email_verified: string;
}
interface GoogleTokensResult {
access_token: string;
expires_in: Number;
refresh_token: string;
scope: string;
id_token: string;
}
interface AccessCodeResult {
result?: GoogleTokensResult;
error?: string;
statusCode?: number;
}
/**
* Retrieves an access code from Google's OAuth 2.0 token endpoint.
*
* @param {string} code - The authorization code received from the OAuth 2.0 authorization server.
* @returns {Promise<AccessCodeResult>} A promise that resolves to an object containing the access code result, error message, and status code.
*
* @throws {Error} If the request fails, an error object is returned with the error message.
*/
const getAccessCode = async (code: string): Promise<AccessCodeResult> => {
const url = "https://oauth2.googleapis.com/token";
const values = {
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectURI,
grant_type: "authorization_code",
};
const params = new URLSearchParams(values).toString();
try {
const response = await axios.post<GoogleTokensResult>(
url,
params,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (response.status !== 200) {
return { error: response.statusText, statusCode: response.status };
}
const result = response.data;
return { result, error: "", statusCode: 200 };
} catch (error: any) {
console.error(error);
return { error: error };
}
}
type UserInfoResult = {
result?: GoogleUserResult;
error?: string;
statusCode?: number;
}
/**
* Fetches user information from the Google OAuth2 API using the provided access code.
*
* @param {string} accessCode - The access token used for authorization.
* @returns {Promise<UserInfoResult>} A promise that resolves to a UserInfoResult object containing the user information or an error.
*
* @example
* ```typescript
* const accessCode = 'your-access-token';
* getUserInfo(accessCode)
* .then(userInfo => {
* if (userInfo.error) {
* console.error('Error fetching user info:', userInfo.error);
* } else {
* console.log('User info:', userInfo.result);
* }
* });
* ```
*/
const getUserInfo = async (accessCode: string): Promise<UserInfoResult> => {
try {
const response = await axios.get<GoogleUserResult>("https://www.googleapis.com/oauth2/v3/userinfo", {
headers: {
Authorization: `Bearer ${accessCode}`
}
})
if (response.status !== 200) {
return { error: response.statusText, statusCode: response.status };
}
const result = response.data;
return { result, error: "", statusCode: 200 };
}
catch (error: any) {
console.error(error);
return { error: error };
}
}
/**
* Handles authentication for an extension by processing the provided authorization code.
*
* @param req - The request object containing the authorization code in the body.
* @param res - The response object used to send back the result.
*
* @returns A JSON response containing either a one-time code for the authenticated user or an error message.
*
* The function performs the following steps:
* 1. Extracts the authorization code from the request body.
* 2. Uses the authorization code to obtain an access token.
* 3. Retrieves user information using the access token.
* 4. Verifies the user's email.
* 5. Checks if the user already exists in the system.
* 6. If the user exists, generates a one-time code for the user.
* 7. If the user does not exist, creates a new user and generates a one-time code for the new user.
* 8. Returns the one-time code or an error message in the response.
*
* @throws Will return an error message and status code if any step in the process fails.
*/
export const authForExtension: AuthForExtension = async (req, res) => {
const { code } = req.body.json
const { result, error, statusCode } = await getAccessCode(code)
if (!error) {
const { access_token } = result as GoogleTokensResult;
const { result: userInfo, error: userInfoError, statusCode: userInfoStatusCode } = await getUserInfo(access_token)
if (!userInfoError && userInfo) {
if (!userInfo.email_verified)
return res.json({ error: "Email is not verified", statusCode: 401 });
const { sub, email, name } = userInfo as GoogleUserResult;
const providerData = {
providerName: "google" as ProviderName,
providerUserId: sub
}
// return res.json({ userInfo });
const existingUser = await findAuthIdentity(providerData);
// return res.json({ existingUser });
console.log(existingUser);
if (existingUser) {
const authId = existingUser.authId;
const oneTimeCode = await tokenStore.createToken(authId)
return res.json({ code: oneTimeCode });
} {
// Create new user
const user = await createUser(
providerData,
JSON.stringify(userInfo),
// User fields
{
email: userInfo.email,
username: userInfo.email
})
const authId = user.auth.id;
const oneTimeCode = await tokenStore.createToken(authId)
return res.json({ code: oneTimeCode });
}
}
else {
return res.json({ error: userInfoError, statusCode: userInfoStatusCode });
}
}
else {
return res.json({ error, statusCode });
}
}
//----
api authForExtension {
httpRoute: (POST, "/auth/extension"),
fn: import { authForExtension } from "@src/auth/extensionAuthApi",
entities: []
}
// ---
// extn/app/popup.js
/* Add handler method to login button which sends message to background script.
The limitation with lauchwebflow is it redirects and closes the popup window and extension
can only listen back from Google oauth redirection in background script
*/
const TopNavBar = () => {
;
const handleLogin = async () => {
try {
await new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type: "LOGIN" }, (response) => {
if (response) {
console.log(response);
resolve();
} else {
reject(new Error('Failed to Login'));
}
});
});
} catch (error) {
throw new Error(`Sign-in failed: ${error.message}`)
}
};
..... // Other part of react code
}
// file extn/services.js
/**
* Launches a web authentication flow using Chrome's identity API.
*
* @param {string} url - The URL to initiate the web authentication flow.
* @returns {Promise<Object>} A promise that resolves with an object containing the authorization code, or rejects with an error object.
* @throws {Error} If the authorization code is not found or if there is no response URL.
*/
const launchWebAuthFlow = async (url) => {
return new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow({
interactive: true,
url
}, (responseUrl) => {
if (responseUrl?.includes('#')) {
const params = new URLSearchParams(responseUrl.split("#")[1])
console.log(params, "params")
const code = params.get("code")
if (!code) {
console.error("Authorization code not found");
reject({ error: "Authorization code not found" });
}
resolve({ code });
} else {
reject({ error: 'No response URL' });
}
});
});
}
/**
* Initiates the OAuth2 authentication flow for a user, exchanges the authorization code for tokens,
* and performs an authentication check to retrieve user data.
*
* @async
* @function saveUser
* @returns {Promise<Object|undefined>} Returns an object containing the session token and user data if successful, otherwise undefined.
* @throws Will log errors to the console if any step of the authentication process fails.
*/
export const saveUser = async () => {
const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth")
const clientId = "<clientId from Oauth credentials>"
// Note: this needs to match the one used on the server (below)
// note the lack of a trailing slash
const redirectUri = chrome.identity.getRedirectURL()
// const redirectUri = `https://${chrome.runtime.id}.chromiumapp.org`
const state = Math.random().toString(36).substring(7)
const scopes = "profile email"
authUrl.searchParams.set("state", state)
authUrl.searchParams.set("client_id", clientId)
authUrl.searchParams.set("redirect_uri", redirectUri)
authUrl.searchParams.set("scope", scopes)
authUrl.searchParams.set("response_type", "code token id_token")
authUrl.searchParams.set("access_type", "offline")
authUrl.searchParams.set("include_granted_scopes", "true")
authUrl.searchParams.set("prompt", "login")
authUrl.searchParams.set("nonce", "1")
const { code, error } = await launchWebAuthFlow(authUrl.href)
if (!code) {
console.error("Authorization code not found");
return;
}
try {
const { data, statusCode, error } = await authExchangeCodeForToken(code)
console.log(statusCode, "statusCode")
console.log(error, "error")
if (error) {
console.error("Error exchanging code for token:", error);
return;
}
if (data) {
console.log(data, "data")
const { code: oneTimeToken } = data;
const { data: sessionExchangeData, statusCode: sessionExchangeStatusCode, error: sessionExchangeError } = await exchangeCodeToSessionId(oneTimeToken)
if (sessionExchangeError) {
console.error("Error exchanging one time token for session ID:", sessionExchangeError);
return;
}
const sessionId = sessionExchangeData.sessionId;
console.log(sessionId, "sessionId")
const { data: userData, statusCode: authCheckStatusCode, error: authCheckError } = await authCheck(sessionId)
if (authCheckError) {
console.error("Error during auth check:", authCheckError);
return;
}
if (authCheckStatusCode === 401) {
setToken('');
chrome.storage.sync.remove(['token'], () => {
console.log('Token removed');
});
} else if (userData) {
console.log(userData, "userData")
return { token: sessionId, user: userData.json }
}
}
} catch (error) {
console.error("An error occurred during the authentication process:", error);
}
}
/**
* Exchanges a one-time code for a session ID by making a POST request to the authentication server.
*
* @param {string} code - The one-time code to be exchanged for a session ID.
* @returns {Promise<Object>} A promise that resolves to the response object if successful, or an error object if there was a problem.
*/
export const exchangeCodeToSessionId = async (code) => {
try {
const response = await fetch('http://localhost:3001/auth/exchange-code ', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ code })
})
return validateResponse(response);
}
catch (error) {
console.error('There was a problem exchanging one time code to session ID', error);
return { error: error.message };
}
}
/**
* Exchanges an google authorization code for an onetime opensaas onetime authentication token.
*
* @param {string} code - The authorization code to exchange for a token.
* @returns {Promise<Object>} A promise that resolves to the response object or an error object.
* @throws {Error} If there is a problem with the fetch operation.
*/
export const authExchangeCodeForToken = async (code) => {
try {
const response = await fetch('http://localhost:3001/auth/extension', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ "json": { code } })
})
console.log(response, "response")
return validateResponse(response);
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
return { error: error.message };
}
}
/**
* Checks the authentication status of a user by validating the provided token.
*
* @param {string} token - The authentication token to be validated.
* @returns {Promise<Object>} A promise that resolves to the response of the validation check.
* If the fetch operation fails, it returns an object with an error message.
* @throws {Error} If there is a problem with the fetch operation.
*/
export const authCheck = async (token) => {
try {
const response = await fetch('http://localhost:3001/auth/me', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
}
})
return validateResponse(response);
}
catch (error) {
console.error('There was a problem with the fetch operation:', error);
return { error: error.message };
}
}
/**
* Validates the HTTP response and returns an appropriate object based on the status code.
*
* @param {Response} response - The HTTP response object to validate.
* @returns {Promise<Object>} A promise that resolves to an object containing either the data and statusCode 200,
* or an error message and the corresponding statusCode.
* Possible status codes and their corresponding error messages:
* - 401: Unauthorized access - perhaps your token is invalid? Try logging in via extension.
* - 429: Too many requests - you are being rate limited.
* - 400: Bad request.
* - 500: Network response was not ok: {response.statusText}.
*/
export const validateResponse = async (response) => {
if (!response.ok) {
switch (response.status) {
case 401:
return { statusCode: 401, error: 'Unauthorized access - perhaps your token is invalid? Try logging in via extension' };
case 429:
return { statusCode: 429, error: 'Too many requests - you are being rate limited.' };
case 400:
return { statusCode: 400, error: 'Bad request' };
default:
return { statusCode: 500, error: `Network response was not ok: ${response.statusText}` };
}
}
console.log(response, "response")
const data = await response.json();
return { data, statusCode: 200 };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment