I have successfully integrated auth0 into my Chrome extension with the help of this community.
Right now I am able to get the auth token of the user from a pop-up window via chrome.identity.launchWebAuthFlow. This login flow works for both email-password and Google login.
Everything is handled in the background script, and the token is saved to chrome local storage.
The current problem I am facing is that if the user enters the wrong email or password, the pop-up window closes abruptly without showing the user an error. When I check the logs in the console, I see a 400 error from auth0 right before the window closes.
Ideally, the pop-up window doesn’t close during an error and auth0 refreshes the page to show that there is an error in the entered user info.
The moment the pop-up window closes, I see the below log in my background service worker console:
chrome.runtime.lastError.message Authorization page could not be loaded.
I am sharing my code below.
let auth0Domain = "dev-<SOME_NUMBERS>.us.auth0.com";
let CLIENT_ID = "<SOME_NUMBERS>";
const AUTH0_AUDIENCE = "https://dev-<SOME_NUMBERS>.us.auth0.com/api/v2/";
const chromeRedirectUrl = chrome.identity.getRedirectURL("");
console.log("chromeRedirectUrl", chromeRedirectUrl);
let nonce = generateRandomString();
let state = generateRandomString();
let authUrl =
`https://${auth0Domain}/authorize?` +
"client_id=" +
encodeURIComponent(CLIENT_ID) +
"&response_type=id_token token" +
"&prompt=login" +
"&redirect_uri=" +
encodeURIComponent(chromeRedirectUrl) +
"&scope=" +
encodeURIComponent("openid profile email") +
"&audience=" +
encodeURIComponent(AUTH0_AUDIENCE) +
"&nonce=" +
nonce +
"&state=" +
state;
function parseRedirectUrl(redirectUrl: string) {
let hashFragment = redirectUrl.split("#")[1];
let params = new URLSearchParams(hashFragment);
let idToken = params.get("id_token");
let accessToken = params.get("access_token");
let expiresIn = params.get("expires_in");
if (!idToken || !accessToken || !expiresIn) {
throw new Error("Missing required parameters in redirect URL.");
}
return { idToken, accessToken, expiresIn };
}
function generateRandomString() {
var array = new Uint32Array(28);
crypto.getRandomValues(array);
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join(
""
);
}
export const launchAuth0 = ({
interactive = true,
}: {
interactive?: boolean;
}): Promise<[string, string]> => {
return new Promise<[string, string]>((resolve, reject) => {
chrome.identity.launchWebAuthFlow(
{ url: authUrl, interactive: interactive },
function (redirectUrl) {
console.log("redirectUrl", redirectUrl);
if (chrome.runtime.lastError) {
console.log(
"chrome.runtime.lastError.message",
chrome.runtime.lastError.message
);
reject(chrome.runtime.lastError);
}
if (!redirectUrl) {
console.log("Authorization failed");
reject("Authorization failed");
return;
}
const { accessToken, expiresIn, idToken } =
parseRedirectUrl(redirectUrl);
chrome.storage.local.set(
{
access_token: accessToken,
id_token: idToken,
expiration_time: new Date().getTime() / 1000 + parseInt(expiresIn),
},
function () {
resolve([accessToken, idToken]); // Here the Promise resolves with a string token
}
);
}
);
});
};
export const logout = (): Promise<void> => {
console.log("logout is called");
let logoutUrl = `https://${auth0Domain}/v2/logout?client_id=${encodeURIComponent(
CLIENT_ID
)}&returnTo=${encodeURIComponent(chromeRedirectUrl)}`;
return new Promise((resolve, reject) => {
chrome.identity.launchWebAuthFlow(
{ url: logoutUrl, interactive: false },
function () {
if (chrome.runtime.lastError) {
console.error(
"chrome.runtime.lastError.message",
chrome.runtime.lastError.message
);
reject(chrome.runtime.lastError);
} else {
// Optionally clear the locally stored token
chrome.storage.local.remove(
["access_token", "expiration_time", "id_token"],
function () {
console.log("User is logged out");
resolve(); // Here the Promise resolves
}
);
}
}
);
});
};
And the background script calls launchAuth0 as below:
import { launchAuth0, logout } from "./auth0";
import { decodeToken } from "react-jwt";
console.log("background script is running");
function processToken(token: string | null, user?: any) {
const message = {
type: "LOGIN_STATUS_CHANGED",
token,
user,
};
chrome.runtime.sendMessage(message, () => {
if (chrome.runtime.lastError) {
console.warn(chrome.runtime.lastError.message);
}
});
// also send this message to all tabs
chrome.tabs.query({}, function (tabs) {
for (var i = 0; i < tabs.length; ++i) {
chrome.tabs.sendMessage(tabs[i].id!, message);
}
});
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
new Promise((resolve, reject) => {
if (request.type === "LOGIN") {
console.log("Background script - LOGIN");
chrome.storage.local.get(
["access_token", "expiration_time", "id_token"],
function (result: {
access_token?: string;
expiration_time?: number;
id_token?: string;
}) {
if (result.access_token && result.expiration_time) {
let current_time = new Date().getTime() / 1000;
if (current_time > result.expiration_time) {
launchAuth0({ interactive: false })
.then(([accessToken, idToken]) => {
const user = decodeToken(idToken || "");
processToken(accessToken, user);
resolve({ token: result?.access_token, user: user });
})
.catch((error) => reject(error));
} else {
const token = result.access_token;
processToken(token);
const user = decodeToken(result?.id_token || "");
resolve({ token: result?.access_token, user: user });
}
} else {
console.log("Background script - LOGIN - no token found");
launchAuth0({ interactive: true })
.then(([accessToken, idToken]) => {
const user = decodeToken(idToken || "");
processToken(accessToken, user);
resolve({ token: result?.access_token, user: user });
})
.catch((error) => {
console.log("Background script - LOGIN - error: ", error);
reject(error);
});
}
}
);
} else if (request.type === "GET_LOGIN_STATUS") {
console.log("Background script - GET_LOGIN_STATUS");
chrome.storage.local.get(
["access_token", "expiration_time", "id_token"],
function (result: {
access_token?: string;
expiration_time?: number;
id_token?: string;
}) {
const user = decodeToken(result?.id_token || "");
resolve({ token: result?.access_token, user: user });
}
);
} else if (request.type === "LOGOUT") {
console.log("Background -- LOGOUT");
logout()
.then(() => {
console.log(
"logout success -- sending message => LOGIN_STATUS_CHANGED"
);
const message = {
type: "LOGIN_STATUS_CHANGED",
token: null,
user: null,
};
chrome.runtime.sendMessage(message, () => {
if (chrome.runtime.lastError) {
console.warn(chrome.runtime.lastError.message);
}
});
processToken(null, null);
resolve({ token: null, user: null });
})
.catch((error) => {
console.log("logout error: ", error);
reject(error);
});
} else {
reject(new Error("Unknown request type: " + request.type));
}
})
.then((result) => {
console.log(
"Request type:",
request.type,
" -- [OK] in background script",
result
);
sendResponse(result);
})
.catch((error) => {
console.log(
"Request type:",
request.type,
" -- [ERROR] in background script",
error
);
sendResponse({ error });
});
return true;
});