Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lcmchris/da979cbbf56c9452b6e5847ece7ee6ca to your computer and use it in GitHub Desktop.
Save lcmchris/da979cbbf56c9452b6e5847ece7ee6ca to your computer and use it in GitHub Desktop.
Sveltekit + Supabase Authentication for Chrome Extension

I am currently using Supabase to build out the Meeting Dolphin Extension. One of the user flows I wanted to create was one where the user:

  1. Installs addon

  2. Website popups

  3. User signs in via OAuth in Website

  4. User session is passed onto the extension

chrome.runtime.onInstalled( 
  chrome.tabs.create({
    url: `AUTH_URL`
  });

Follow the svelkite helpers setup. For Google OAuth

<button
  on:click={async () => {
    const response = await $page.data.supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/redirect`,
        queryParams: { access_type: 'offline', prompt: 'consent' },
        scopes:
          'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events'
      }
    });
  }}>Continue with Google Calendar</Button
>

The main difficultly was the passing of the tokens - step 4. This is done using sendMessage + onMessageExternal.

In "manifest.json"

"externally_connectable": {
    "matches": [
        "*://meetingdolphin.com/*",
        "http://127.0.0.1:5173/*"
    ]
},

In "background.js", we set the following local storage to prepare for receiving message from our website.

const options = {
    auth: {
        autoRefreshToken: true,
        persistSession: true,
        detectSessionInUrl: true,
        storage: {
            async getItem(key: string): Promise<string | null> {
                // @ts-ignore
                const storage = await chrome.storage.local.get(key);
                return storage?.[key];
            },
            async setItem(key: string, value: string): Promise<void> {
                // @ts-ignore
                await chrome.storage.local.set({
                    [key]: JSON.parse(value)
                });
            },
            async removeItem(key: string): Promise<void> {
                // @ts-ignore
                await chrome.storage.local.remove(key);
            }
        }
    }
}

import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
    import.meta.env.VITE_SUPABASE_URL,
    import.meta.env.VITE_SUPABASE_ANON_KEY,
    options
);

chrome.runtime.onMessageExternal.addListener(async ({ message, session }) => {
    console.log("External message ", message)

    if (message === "SIGNED_IN") {
        const { data, error } = await supabase.auth.setSession({
            access_token: session['access_token'],
            refresh_token: session['refresh_token'],
        });
        chrome.storage.local.set({ session: session }, () => { })

    }
    else if (message === "SIGNED_OUT") {
        const response = await supabase.auth.signOut()
    }
}
)

In website, "+layout.svelte", we send a message and the current session to the extension (based on id) when there is an AuthChange

<script lang="ts">
  import './styles.css';

  import { invalidate } from '$app/navigation';
  import { onMount } from 'svelte';
  import type { LayoutData } from './$types';
  import { publishAuthMessage } from '$lib/assets/js/auth';

  export let data: LayoutData;

  $: ({ supabase, session } = data);
  onMount(() => {
    const {
      data: { subscription }
    } = supabase.auth.onAuthStateChange((event, _session) => {
      console.log(_session);
      if (event === 'SIGNED_IN') {
        publishAuthMessage('SIGNED_IN', _session);
      }
      if (event === 'SIGNED_OUT') {
        publishAuthMessage('SIGNED_OUT');
      }

      if (_session?.expires_at !== session?.expires_at) {
        invalidate('supabase:auth');
      }
    });

    return () => subscription.unsubscribe();
  });
</script>

<slot />

with "auth.ts"

export const publishAuthMessage = async (message: string, session?) => {
    console.log('publishAuthMessage', message, session);
    return await chrome.runtime.sendMessage(PUBLIC_EXTENSION_ID, {
        message,
        session
    });
}
@csenio
Copy link

csenio commented Jul 16, 2023

thanks for sharing this. I've been using something similar but calling

   const { data, error } = await supabase.auth.setSession({
            access_token: session['access_token'],
            refresh_token: session['refresh_token'],
        });

in my background script will actually invalidate the token on my website so users will have to log in again. Is that not an issue you had?

@lcmchris
Copy link
Author

@jescowuester it's not a problem I've met. Would you like to share your code?

@csenio
Copy link

csenio commented Jul 25, 2023

@lcmchris was using your exact code.
in my case I was also using the website as a dashboard, and when the website did a refresh, supabase would issue a new refresh and access token. Since supabase uses rolling refresh tokens, this would invalidate my token in chrome.storage.local.

I managed to fix this by instead storing the tokens in my website cookies.
That way there is a single source of truth that the website and extension can share. And no matter who uses the refresh token, they just put a new one back in the cookies.

@remusris
Copy link

@lcmchris was using your exact code. in my case I was also using the website as a dashboard, and when the website did a refresh, supabase would issue a new refresh and access token. Since supabase uses rolling refresh tokens, this would invalidate my token in chrome.storage.local.

I managed to fix this by instead storing the tokens in my website cookies. That way there is a single source of truth that the website and extension can share. And no matter who uses the refresh token, they just put a new one back in the cookies.

In a similar situation, I have a chrome extension and accompanying webapp but I didn't consider the possibility that the refresh token event from the webapp could break the auth from the chrome extension. You mind sharing your code on how you use the cookie implementation for auth?

@csenio
Copy link

csenio commented Jul 29, 2023

@remusris there is a very rough version here: https://github.com/jescowuester/supabase-auth-helper-webextension

You'll still have to set up some kind of notification system between extension and website because they can't automatically detect when a cookie changes. In my case I just "poke" the other one onAuthStateChange and then it can check the cookies for the actual auth state

@remusris
Copy link

@remusris there is a very rough version here: https://github.com/jescowuester/supabase-auth-helper-webextension

You'll still have to set up some kind of notification system between extension and website because they can't automatically detect when a cookie changes. In my case I just "poke" the other one onAuthStateChange and then it can check the cookies for the actual auth state

I've been having an auth issue in my web app and now I'm thinking that it was from the chrome extension invalidating the refresh token from the webapp. I'd get the error from supabase that my refresh token was expired and have not been able to figure out from where, thanks for sharing.

@remusris
Copy link

@lcmchris

How did you figure out that you need getItem, setItem, and removeItem for interfacing with the storage requirements of the supabaseJS library? I tried digging into that library but I couldn't find those methods anywhere.

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

@lcmchris
Copy link
Author

@remusris getItem, setItem, and removeItem is based off on the web Storage API.
If you dig through the library - it goes from:

export type SupabaseClientOptions<SchemaName>

type GoTrueClientOptions = ConstructorParameters<typeof GoTrueClient>[0]

export interface SupabaseAuthClientOptions extends GoTrueClientOptions {}
export default class GoTrueClient {
...
 protected storage: SupportedStorage
...
}

export type SupportedStorage = PromisifyMethods<Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment