I recently built a Chrome extension for Katalog, the audio-first read-it-later app. It's similar to Pocket, but focused on listening to the articles you save. When users find an interesting piece of content, they can save it with the extension. Katalog then parses it and generates optimised audio narration.
I needed the extension to recognize users who were already logged into the web app, without making them log in again. I also wanted to avoid duplicating the authentication flow or storing tokens in multiple places. Here's how I approached it, what worked, and what I'd do differently next time.
1. Why I chose cookie-based auth
I tried a few different approaches before settling on cookie-based authentication. At first, I considered integrating the Supabase SDK directly into the extension, or building a custom login flow inside the extension. Both options felt too heavy and required me to manage tokens separately from the web app.
Instead, I realized I could just check for the presence of the Supabase auth cookies that are set when a user logs into the web app. If those cookies exist, the extension can assume the user is authenticated. This way, I could reuse the existing login flow and avoid building extra UI or logic.
2. Architecture overview
Here’s a simple diagram of how things fit together:
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| Chrome | <----> | Web App | <-----> | Supabase |
| Extension | | (React, Magic | | (Auth, DB) |
| | | Link Auth) | | |
+-------------------+ +-------------------+ +-------------------+
| ^
| |
| (Checks for Supabase |
| cookies on web app |
| domain) |
+------------------------------+
- The extension checks for Supabase cookies on the web app’s domain.
- If authenticated, it calls a web app endpoint (e.g.,
/api/parse-article
) to perform actions. - The web app handles authentication and talks to Supabase.
3. Setting up Supabase auth in the web app
I use Supabase's Magic Link authentication in the web app that I built with React Router v7 (framework mode). The web app also uses server-side rendering, so I handle authentication with middleware that intercepts requests and checks for Supabase cookies in the headers.
You can set up Supabase authentication for SSR with this guide. The important part is that Supabase sets its own cookies when a user logs in. You don't need to do anything special in the extension to create or manage these cookies.
4. Chrome extension: cookie detection
The core of the authentication check happens in the background script. Here’s the function I use to check for the presence of Supabase’s auth cookies:
// background.ts
async function hasAuthCookies(domain: string): Promise<boolean> {
const cookies = await browser.cookies.getAll({ domain });
const authCookies = cookies.filter((cookie) =>
// These env vars are set to the standard Supabase cookie names
cookie.name.includes(import.meta.env.VITE_KATALOG_AUTH_TOKEN_COOKIE) ||
cookie.name.includes(import.meta.env.VITE_KATALOG_AUTH_TOKEN_CODE_VERIFIER_COOKIE)
);
return authCookies.length > 0;
}
I rely on environment variables for the cookie names, but these are just the defaults from Supabase. You can inspect these cookies in the browser's storage after logging in. Since I'm using Vite Web Extension plugin, I store these variables in a .env
file. Vite automatically loads any environment variables prefixed with VITE_
, making them available through import.meta.env
in your code.
# .env
VITE_KATALOG_AUTH_TOKEN_COOKIE=sb-access-token
VITE_KATALOG_AUTH_TOKEN_CODE_VERIFIER_COOKIE=sb-refresh-token
5. Manifest and permissions
Here’s the relevant part of my manifest.json
for Chrome:
{
"manifest_version": 3,
"name": "Katalog - Save any article to listen later",
"version": "0.0.3",
"permissions": ["activeTab", "tabs", "cookies"],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "src/background.ts"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content.ts"]
}
]
}
⚠️ An important note: I ran into issues when I tried to restrict host_permissions
to just my web app’s domain. Chrome wouldn’t return the cookies I needed. Only after allowing access to all domains did it work. This feels like a bug in Chrome’s extension API. I couldn’t find any alternatives. However, as long as you describe the intent for using such broad host_permissions
clearly when submitting the extension, I haven’t had any issues with publishing updates.
6. The login flow
When a user clicks the extension icon, the extension checks for the auth cookies. If they’re missing, it opens the login page in a new tab. I added a query parameter so the login page can show a message specific to extension users.
// background.ts
if (!hasAuth) {
await browser.windows.create({
url: import.meta.env.VITE_KATALOG_BASE_URL + "login?browser-extension=true",
focused: true,
type: "normal",
});
}
Right now, there’s no way for the extension to know immediately when the user finishes logging in. You have to click the extension icon again after logging in to save the current URL. I’d like to improve this in the future, maybe by using browser messaging or a custom event.
7. Content script and UX
When the extension is used on a page, it injects a content script that displays a floating element. This shows the progress as the article is being parsed and saved. I kept this UI minimal on purpose—no extra popups or login prompts.
8. Server-Side: CORS and Credentials
On the server side (web app), I had to make sure that credentials are allowed in CORS headers. Here’s a snippet from my Express server setup:
// server.js
app.use((req, res, next) => {
res.header("Access-Control-Allow-Credentials", "true");
// ...other headers
next();
});
This is important so that the extension can make authenticated requests to the web app’s API endpoints.
9. Security and limitations
There are a few things to keep in mind with this approach:
- The extension can only access cookies that aren’t
HttpOnly
. Supabase’s cookies are accessible in this way, but if you use a different auth provider, check their cookie settings. - By allowing access to all domains in
host_permissions
, you’re giving the extension broad access. This is something to be aware of, and you might want to review the permissions model if you’re building something more sensitive. - There’s no real-time notification to the extension when the user logs in. The user has to click the extension again after logging in.
10. What I’d improve next
- I’d like to add a way for the extension to know when the user has finished logging in, maybe by using browser messaging or a custom callback.
- I’d also like to tighten up the permissions if Chrome’s API allows it in the future.
11. Conclusion
This approach let me keep the extension simple and avoid duplicating authentication logic. I didn’t have to build a separate login UI or manage tokens in two places. If you’re already using Supabase (or any provider that uses cookies for auth), this is a lightweight way to add authentication to your extension.
Would you use cookie-based authentication for Chrome extensions? I'd love to hear your thoughts and feedback 🙌
Top comments (1)
When your extension sends a request to your API, how does the server know it’s coming from your extension? Also, do you send the request from the content script or the background script?