Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent behaviour of auth in Supabase Client between tab/popup and service-worker #456

Closed
gprieto opened this issue Sep 26, 2022 · 6 comments
Labels
bug Something isn't working

Comments

@gprieto
Copy link

gprieto commented Sep 26, 2022

Bug report

Describe the bug

I am developing a chrome extension with auth using an external provider.

  1. I log in my users from the popup, works fine: the session is stored in chrome.local.storage.
  2. BUT in the service-worker script, supabase.auth.session() returns null

Is there another way to implement this that I am not seeing?

supabase.ts:

import { createClient, type SupportedStorage } from '@supabase/supabase-js';
export type { Session } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

const chromeStorageInterface: SupportedStorage = {
	async getItem(key: string): Promise<string | null> {
		const storage = await chrome.storage.local.get(key);
		return storage?.[key];
	},
	async setItem(key: string, value: string): Promise<void> {
		await chrome.storage.local.set({
			[key]: JSON.parse(value)
		});
	},
	async removeItem(key: string): Promise<void> {
		await chrome.storage.local.remove(key);
	}
};

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
	multiTab: true,
	localStorage: chromeStorageInterface,
        persistSession: true,
});

Also both the popup and the service-worker implement the onAuthListener.

Expected behavior

As both the popup and the service-worker supabase clients share the same storage, they should both have the same session.

Additionnal information

I have tried manually singing in from the service-worker with the following snippet:

const setSession = async (config:ChromeConfig) => {
	if (config['supabase.auth.token']?.currentSession.refresh_token) {
		console.log('Logging in to Supabase using localstorage session');
		const {error} = await supabase.auth
			.signIn({
				provider: 'notion',
				refreshToken: config['supabase.auth.token'].currentSession.refresh_token
			})
		if (error) throw new Error(error.message)
	}
};

The sign in works and then the refresh_token becomes invalid but the updated refresh_token is not persisted back to chrome.storage.local so once the tokenexpires the user is logged out.

@gprieto gprieto added the bug Something isn't working label Sep 26, 2022
@soedirgo soedirgo transferred this issue from supabase/supabase-js Sep 27, 2022
@miguelespinoza
Copy link

Hey @gprieto, glad to hear there are more extension developers using supabase.

I've only had experience with Google and MagicLink, but I would think the auth flow should be the same for any of these providers.

So with Google you get the provider_token, but that's passed to a supabase auth redirect which then redirects to your website. I might have overcomplicated this process but here's what I have for my extension:

  1. extension opens app.site.com/auth a React web app hosted on Netlify
  2. app.site.com/auth initializes a supabse instance to authenticated with Google
  3. Select Google account
  4. Google redirects to supabase with provider_token
  5. supabase redirects to app.site.com/auth/redirect with supabase specific accessToken and refreshToken.
  6. In app.site.com/auth/redirect call getSessionFromUrl
  7. app.site.com/auth/redirect messages the accessToken to the extension using chrome.runtime.sendMessage
  8. extension receives the accessToken and creates the session for the extension. This is where things get tricky with v1 vs v2.

So, you're using v1. In v2, things change. You might have seen setAuth, this is what I originally used, but I recently migrated to v2. In v2, there's only one method setSession(refreshToken). Which unfortunately revokes the current refreshToken and creates a new one, that's another problem for another discussion. But this flow works. Once you've called setSession with your refreshToken the session will be persisted into storage.local.

In v2, things will just work regardless of what area of the extension you're running a supabase request, popup, content_script, or background. It works because supabase fetch checks auth session for a valid accessToken.

In v1, things don't work as you'd expect. First of all multiTab option does not work with service worker based backgrounds. If you look at the multiTab logic, it uses window.addEventListener('storage') which as you'll know is not supported in a service worker. So this is likely why you're having issues sharing the session in storage.local.

My recommendation is to upgrade to v2. There are still a few gotchas, you can see my journey in this PR I submitted: #444. I've since reworked my strategy, so it should help you too.

One thing I still have to work out is how to sync the session between the web app and the extension. Right now the web app is just a facade for authentication, which relays the token to the extension, but in the future I want them to work in conjunction. So that'll be a tricky thing to work through when I get to it. I put some thought, and the best I could come up with is to share the session through cookies. That's probably what I should have done since day 1, but I'm going with localStorage for now. I'm sure I can migrate that from chrome.storage.local to chrome.cookies. So just an FYI

@gprieto
Copy link
Author

gprieto commented Sep 28, 2022

Hey @miguelespinoza, I appreciate your detailed answer, many thanks!

I really hope that v2 will solve this, I will look at your PR more in-depth in the coming days when I find the time.

Regarding your process:

I found it is much simpler to launch the Supabase log-in process with a provider directly from the extension, with a redirect to the extension. A couple of cave-heats on that:

  1. You need to be on a tab/window, not in the pop-up, so the page doesn't close while you authenticate with the external provider (I read this somewhere, not 100% sure)
  2. If you use the Supabase UI - you cannot currently whitelist a chrome-extension:// URI, but there has been a PR merged (#8533) that should be in effect in the next release.
    In the meantime, the trick is to redirect to an app.site.com/auth page that reads the hash in the URL, and simply redirects to your extension with the same hash - so that Supabase gets the session from the URL.

I just found a working fix for keeping the service-worker logged in (with v1)

The trick is to call a setSupabaseSession function before any usage of supabase. This effectively works around the bugs I encountered:

  1. In the service-worker any local changes to the session are not persisted in localStorage.
  2. In the service-worker at initialization the session is not read from localStorage.
  3. In the service-worker external changes to localStorage do not trigger an event.
const setSupabaseSession = async (config: ChromeConfig) => {
	if (config['supabase.auth.token']) {
		const session = config['supabase.auth.token'].currentSession;
		if (
			session.expires_at &&
			session.refresh_token &&
			(isPast(session.expires_at) || !supabase.auth.session())
		) {
			// If no session or token has expired, getSession using the refresh_token
			console.log('Getting new Supabase session - refreshing Token');
			const { session: newSessionData, error: newSessionError } = await supabase.auth.setSession(
				session.refresh_token
			);
			if (newSessionError) throw newSessionError;
			// Write back the session, with the new refresh_token, to localStorage
			chrome.storage.local.set({ 'supabase.auth.token': { currentSession: newSessionData } });
		} else console.log('Supabase session valid');
	} else {
		throw new Error('No Supabase session in config');
	}
};

@wd021
Copy link

wd021 commented Oct 15, 2022

are you calling setSupabaseSession with what you store in chrome.storage.local?

Doesn't this setup result in updating refresh_tokens on every function call. setSession will call the api..

@gprieto
Copy link
Author

gprieto commented Oct 18, 2022

Update: it seems that v2 solves this problem - and the session is properly written to and read from the specified storage.

@kangmingtay
Copy link
Member

fwiw, i think local storage is not accessible from the service worker based on the mdn docs

@gprieto
Copy link
Author

gprieto commented Oct 26, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants