Syncing authentication state between extension and web apps
Learn how to share authentication sessions across your web app and extension
Have you ever wondered how does extensions sync the authentication state with the web app so that if you log in on the web app, it authenticates you automatically in the extension?
Introduction:
In terms of UX, it isn't the best thing to make your customer log in two times (extension and web app), so it would be great if you sync the login session between both, so that you make it easier for your user to use your product.
But how can we do so? that is what we are going to know in this blog
Browser Messaging System:
A browser messaging system is a way to communicate between extensions and websites. It is mostly used to exchange messages to do specific actions. Of course, we will need two things to use the messaging system:
- listener (receiver)
- sender
A listener could be an extension service worker (a background script) to take action once it receives a message. It could also be a web app (we are going to discuss this deeper in this blog) while the sender could be a content script (extension script that is used to interact with the page DOM) or it could also be a web app.
For instance, we could send a message from the extension content script to send cookies to our backend
// content script
chrome.runtime.sendMessage({
action: 'SEND_COOKIES',
payload: document.cookie
}, (response) => {console.log(response.ok)});
// service worker - background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.action) {
case 'SEND_COOKIES':
// send response is a callback for returning a response to the message sender
sendCookies(request.payload)
.then(r => r.json())
.then(data => sendResponse(ok: data.ok));
// return true to keep the messaging port until the api response is sent
return true;
default:
break;
}
});
As you can see, it is pretty straightforward, we send the message and the receiver does the intended action
This was a brief introduction about how messaging works in general, now let's head to the main point.
Syncing Login Sessions
In order to send messages from our web app to the extension, we will use cross-messaging. It is the same as normal messaging, but we will have to specify the sender id (extension id) and the web app URL.
Theoretical part
We will need to use pass the access token either from the web app or from the extension (depending on where the first authentication happens) along with the message, and then the receiver will authenticate with it.
Code implementation
In this blog, I am going to use Firebase to make things simple
Assume we have logged in on our web app, and now we need the extension to log in as well automatically, so we are going to send a message from the web app once the user has logged in along with the custom access token
Note: we will need an API to provide us with a custom access token for the account, but we are going to use a callable function to use it in our Firebase app
On the web app, we will send the custom token request once the user is authenticated
// web app
function login() {
signInWithEmailAndPassword(auth, email, password).then((result) => {
sendCustomTokenToExtension();
})
}
function sendCustomTokenToExtension() {
const functions = getFunctions(getApp());
// callable function that runs on the cloud
const createCustomToken = httpsCallable(functions, "createCustomToken");
export async function sendCustomTokenToExtension() {
// create a custom token firebase
const userId = auth.currentUser?.uid;
const { data } = await createCustomToken(userId);
// send a message to the extension service worker
window.chrome.runtime.sendMessage(
process.env.REACT_APP_EXTENSION_ID,
{ action: "SIGNIN_WITH_CUSTOM_TOKEN", data }
);
Create custom token callable function
// index.js (cloud function)
exports.createCustomToken = onCall(async (data, context) => {
if (!context.auth) {
throw new HttpsError("failed-precondition", "You are not authenticated");
}
const uid = context.auth.uid;
try {
const customToken = await auth().createCustomToken(uid);
return { status: "ok", token: customToken };
} catch (err) {
return { status: "error", message: err.message };
}
});
finally, you will listen to messages from your extension service worker
//* LISTEN FOR MESSAGES FROM WEBPAGE
chrome.runtime.onMessageExternal.addListener(function (
request,
sender,
sendResponse
) {
switch (request.action) {
case 'SIGNIN_WITH_CUSTOM_TOKEN':
signinWithToken(request.data, sendResponse);
return true;
default:
break;
}
});
async function signinWithToken(data, sendResponse) {
const { token } = data;
// send log in request
signInWithCustomToken(auth, token)
.then((user) => {
sendResponse({ success: true, user })
})
.catch((err) => {
sendResponse({ success: false, message: err.message });
});
}
You will have to specify your domain to the extension manifest.json. This enforces security as it ensures that the connection is initiated from your web app to your extension only, otherwise, it could be a nightmare because malicious extensions could be able to listen for any messages and steal the access token.
// manifest.json
// this will let chrome pass the sendMessage API to your webpage
"externally_connectable": {
"matches": ["https://example.com/*"]
},
This was all about logging in to the user. You could do the same for the logout syncing so that when the user is logged out on the web app, you send a message to log out onto the extension.
What about authenticating on the extension first? Well, you will just do a small tweak, so that every time the user visits the web app, you send a message to the extension (if he was not authenticated on it) to check if the user is authenticated on the extension, and if so, then you will call the callable function from there, and pass it to the web app.
Hope you found this article useful to you. Please feel free to share it with your connections. See you at the next one!