clerk / clerk-chrome-extension-starter

This starter project shows how to use Clerk authentication in a React based Chrome Extension.
32 stars 8 forks source link

Get clerk user auth in Background or Content scripts #4

Closed jellohouse closed 11 months ago

jellohouse commented 1 year ago

In my extension Popup i have the <ClerkProvider> component wrapping the app.

I'm able to login and use all the hooks from the popup and components in the popup.

But now i want to access the logged in user session from my content script and background script.

How can i achieve this?

tolgaouz commented 11 months ago

I also have a very similar use-case. Were you able to achieve what you needed @jellohouse?

jellohouse commented 11 months ago

I ended up using const { getToken } = useAuth(); in my popup and then saving the result from const userAuthToken = await getToken(); to chrome localstorage.

Then in the background/content i just get the auth token from localstorage.

But ideally there would be another way provided by Clerk.

tolgaouz commented 11 months ago

Yes, I agree. Thank you for the reply @jellohouse!

zacharyblank commented 11 months ago

Isn't the issue with this that the token expires and there is no clean way to refresh it?

I have the same use case of needing auth in the background script.

Here is my flow:

  1. user logins in on my website
  2. The extension is already installed and running and I have a content script with which then gets authenticated at the same time as the website session (because they're essentially on the same site as the content script is injected. into the site. The problem with this is that the cookies are set for this domain only, in theory. I am seeing that the content script stays authenticated on other sites but not all. I haven't found a pattern yet.
  3. Having the content script authenticate is a bogus pattern IMO because the cookies are set on other domains (for each site the extension runs on) and very prone to leak/attack/etc.
  4. Instead, the background script needs to auth and the content script uses the background script as a proxy to any protected resources.
LekoArts commented 11 months ago

Hi, thanks for the issue!

We're not actively monitoring issues in starter repositories and as such I'd recommend asking your question either in GitHub Discussions or on our Discord server.

Thanks!

jellohouse commented 11 months ago

Agreed @zacharyblank . This is actually what i've tried to do now: Start the clerk instance from the background script.

Then in the popup I wanted to use <ClerkProvider Clerk={clerk} navigate={(to) => navigate(to)}> .... </ClerkProvider> where clerk is the instance from the background script. However, I'm getting an error from clerkjs that window is undefined from the background script.

And yes, now I realized that I am facing this issue with my previous attempt/solution, the auth token is only valid for about a minute and then it stops working.

Still looking for a solution... open to any suggestions.

zacharyblank commented 11 months ago

I got this working with the following approach:

  1. User installs extension and is taken to a website to authenticate. This is a regular 'ol Nextjs app using Clerk
  2. The website sends a message to the window when authentication happens:
window.postMessage({
    isSignedIn: true
})
  1. A content script in the extension is listening for this message:
window.addEventListener("message", async (event) => {
  if (event.data.isSignedIn) {
    const response = await sendToBackground({
      name: "set-session",
      body: document.cookie
    })
  }
})
  1. That content script sends all the newly set cookie data to a background service worker to be persisted.
  2. At this point the background service worker can interact with clerk via the backend API to do things like verify the session, etc.
  3. Then, whenever a valid token is needed in the content script, the content script simply asks the background script for it and the background script can do whatever it needs to do to provide it. (pull it from storage, generate a new one using the Clerk Backend API, etc.)

I hope this helps!

jellohouse commented 11 months ago

Ok i see when i do console.log(document.cookie) from the content script, there is a cookie called __clerk_db_jwt

Assuming i have that cookie value in my content/background scripts, how would i get a clerk auth token for the user logged in to my extension popup?

Is there a way to start a new clerk instance with that cookie and get the user auth token? Or any way to start clerk in the background with a session from the popup?

P.S. I don't have a website and i would like to avoid it if possible...

zacharyblank commented 11 months ago

You can likely get away with the same pattern without a hosted site of your own if you want to use chrome pages (hosted by the browser itself and packaged with your extension).

instead of __clerk_db_jwt look at the __session cookie. Once you have that you can create any number of tokens for any jwt templates you want using the Clerk Backend API. This is what I do. Store the session ID in the background service and then generate a new token whenever one is needed and pass that new token to the content script.

This is part of a Clerk backend interface that I wrote that handles creating new tokens. It isn't perfect but hopefully gives you an idea:

import type { Clerk } from "./clerk"
import jwt from "jsonwebtoken"

export class Session {
  clerk: Clerk

  constructor(clerk: Clerk) {
    this.clerk = clerk
  }

  async getToken(template: string): Promise<{ jwt: string }> {
    var id: string
    try {
      id = (
        (await this.clerk.storage.get("session")) as {
          sid: string
        }
      ).sid
    } catch (e) {
      console.error("Invalid or no session found in local storage")
    }

    // check if there is an existing token first
    var token = await this.clerk.storage.get(`${template}_token`)

    if (token) {
      const decoded = jwt.decode(token)

      if (decoded.exp) {
        const currentTimestamp = Math.floor(Date.now() / 1000) // Convert milliseconds to seconds
        if (decoded.exp > currentTimestamp) {
          return { jwt: token }
        }
      } else {
        return { jwt: token }
      }
    }

    var { jwt: token } = (await this.clerk._makeRequest(
      `/sessions/${id}/tokens/${template}`,
      {},
      "POST"
    )) as { jwt: string }

    await this.clerk.storage.set(`${template}_token`, token)

    return { jwt: token }
  }

  async revoke(id: string) {
    this.clerk.storage.clear()
    return await this.clerk._makeRequest(`/sessions/${id}/revoke`, {}, "POST")
  }

  async get(id: string) {
    return await this.clerk._makeRequest(`/sessions/${id}`, {}, "GET")
  }
}

I am interested to know if you get it working!

jellohouse commented 11 months ago

Ok so i would need to call this endpoint https://api.clerk.com/v1/sessions/{session_id}/verify

Described here https://clerk.com/docs/reference/backend-api/tag/Sessions#operation/VerifySession

I tried setting the session ID in path and body token to the value of __session from the popup.

But i see the endpoint requires an auth token in headers. I tried setting auth as the __clerk_db_jwt but that didn't work... I'm getting error 401 unauthorized.

How do i get the Bearer auth token for the request?

zacharyblank commented 11 months ago

Sorry for the delayed response. Call the Clerk Backend API with your clerk api token. Not the user's auth token :)

shaezzy commented 9 months ago

Please do not include your secret key in your chrome extension's source code!!

Publishable key: This key should be used in your frontend code, can be safely shared, and does not need to be kept secret. Secret key: These are the secret keys to be used from your backend code. They are sensitive and should be deleted if leaked.

Chrome extensions entire source is available to the browser, allowing anyone to view the source and steal your client secret.

The proper way to accomplish this in chrome extensions, native apps, mobile apps, SPA's etc where a client secret cannot be securely stored is using PKCE flow.

https://oauth.net/2/pkce/

It looks like Clerk does support PCKE now, but have not tried this myself. https://clerk.com/docs/advanced-usage/clerk-idp