Reactively authenticating organization through ctx.auth
I'm using Convex with Clerk, and wanting to authenticate that the user is only querying data related to their active organization. I set up JWT to include the Clerk organization ID through an unused paramater ("language") which is a little hacky, but it works to get me the information. But what I noticed is that this is not reactive. If I use the organization switcher, identity.language does not update, so the query retains the information from the first organization.
Any way around this, or better path to take? Thanks!
Relevant code:
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
return null;
}
const organization = await getOneFromOrThrow(
ctx.db,
"organizations",
"by_clerkId",
identity.language
);
12 Replies
cool workaround with the
language
field! Unfortunately JWTs aren't re-issued reactively. If you want it to be reactive, you can store the data in a convex table and keep it updated with a clerk webhook https://clerk.com/blog/webhooks-data-sync-convexClerk Webhooks: Data Sync with Convex
This post covers how to synchronize user data from Clerk into a Convex database using Webhooks.
@lee Thanks for the response. I might be missing something, but as far as I can tell, there's no Clerk webhook for a user's active organization. It isn't included in the user or the organization update webhook that I can see. As far as I can tell, this is only an issue when the organization is switched, so for now I think I'll probably just force a refresh on change. Outside of an edge case like having just been removed from an organization, I don't believe there's a security concern because we know they have permission for that data.
I haven't used this feature in Clerk so i might be wrong, but
organizationMembership.created/deleted/updated
sounds like it might be what you're looking for https://clerk.com/docs/integrations/webhooks/overviewWebhooks overview | Clerk
Clerk webhooks allow you to receive event notifications from Clerk. Clerk will send a POST request to a URL you specify when certain events happen in your Clerk account.
But also waiting for a refresh sounds reasonable, as it's probably not a security concern for the authenticated organization to be slightly stale
(and it's also possible Clerk's discord will know better than us. We integrate with them, but they know their APIs better than us 🙂 )
I was a little curious about this data flow so did a quick look at the Clerk docs. I haven't tried this myself but this is what I would try to do:
Use the clerk provided
useOrganization
hook (https://clerk.com/docs/references/react/use-organization#use-organization-returns) instead to dynamically get active organization that you can pass to convex queries. It won't be part of your ctx.auth.getUserIdentity()
. The reality is that "active" organization isn't really an immutable part of user identity, it's just your application state.
I would then use the Convex custom function here to inject the currently selected organization into your Convex function ctx objects: https://stack.convex.dev/custom-functions#modifying-the-ctx-argument-to-a-server-function-for-user-auth. I'd probably create an application specific react hooks that combines the Clerk userOrganization
and Convex useQuery
etc. to make this feel automatic.useOrganization() | Clerk
Clerk's useOrganization() hook retrieves attributes of the currently active organization.
Customizing serverless functions without middleware
Re-use code and centralize request handler definitions with discoverability and type safety and without the indirection of middleware or nesting of wr...
Ok so Lee pointed out that if this is a way for you to gate that a user doesn't have access to an organization they aren't part of then my suggestion can be problematic
What you need to store on the Convex side somehow (via the webhooks that Lee mentioned) is which organizations the user has access to or not so you can do that check in your Convex functions.
Thanks @lee and @Indy for the input. I was passing the active org to my query through useOrganization, but realized that wasn't a secure way to gate it, so that's when I moved to the JWT token method. For security sake, as you suggested I can create a organizationMember table from Clerk's webhook's and check against that, was just trying to streamline and avoid any unneeded queries. But I'll look into adding the to the ctx argument. A custom function was the step I was prepping this for regardless to avoid boilerplate in all of my queries.
P.S. really loving Convex. Genuinely hard to imagine moving back to the old way of handling backend.
Your hack for forcing a page refresh on a org change might be ok if you want to avoid all this. But I do agree that the "right way" is to get as much of the data for this in Convex.
Fundamentally what Clerk is providing is sort of an authorization framework with all this. The reality is authorization is meant to protect your core application data. Since your core application data is in Convex, you need to actually get all the authorization metadata in the same place as the application data you're protecting.
Glad you're enjoying Convex!
So are you now updating your users table with your currently active org when you setActive on a org? I seem like I'm in a similar situation where i need the current org available to all my functions. I already have a webhook to create orgs on covex, but there is no webhook for every time a user swiches their org if i am understanding.
I confirmed with Clerk that there is no webhook for the active org switching, and no way to get that information upon change.
Here's where I ended up: I have the code below as a customFunction that pulls the user and active organization from the session details into the context. The trick, as I mentioned in my first post, is that I updated Clerk's JWT token to send the active organization ID through the "language" parameter.
So replacing all of my mutations with mutationWithOrganizationUser (and likewise with a separate custom query) I'm able to get the org ID and user ID through context.
The only caveat I've run into is that it is not reactive to a change in the active organization. So if you're feeding the org ID directly as a argument (which is not secure) changing the active org would trigger a re-render from convex. The session data does not do that. So my solution was just to force a redirect to my homepage on changing your org, which works just fine so far in testing.
Two other notes:
1. For security purposes, I will probably also use the OrganizationMembership webhook and store that in convex to double check that the user is a member of the organization that I get through the clerk session data. I'm not sure if that's necessary, as the clerk webhook is being validated, but I'm not an expert on that by any means.
2. Instead of redirecting, you could also use the organization id in your urls (like /[orgId]/dashboard) and use then ensure that the active org matches the orgId or else redirect.
I appreciate the reply, I was just thinking of putting the active org id in the users table, and that is always available as most functions get wrapped in auth the convex ents way. Why not just update the users table if you use set active... But with clerk its possible they can run switchactive org with a invite or something.. so maybe that is not the best plan. Then again i have almost no reliance on using their built in features now..
Yeah, if you’re using custom components for the user to switch, that sounds like a good solution.