RJ
RJ2y ago

Convex tokenIdentifier vs Clerk user ID

Why does Convex use the tokenIdentifier instead of the Clerk user ID on the UserIdentity in Convex functions? What exactly is the tokenIdentifier (at least in relation to the Clerk user ID)? I did look at the OpenID Connect spec for answers, but was not enlightened 😄 I'm also aware none of this may be specific to Clerk, but that's what I'm using.
18 Replies
Michal Srb
Michal Srb2y ago
Good question! The tokenIdentifier is a combination of the issuer (your Clerk URL) and the Clerk ID. We don't currently expose the "subject" ID, but what you can do is edit the JWT template on Clerk dashboard and pass the "user.id" shortcode in another valid field, like "nickname". "nickname": "{{user.id}}", We should expose the "subject" that's already present, I'll work on getting that fixed. Thanks for the great feedback, keep it coming!
RJ
RJOP2y ago
Thanks @Michal Srb! It looks like the subject would be the Clerk user ID (https://clerk.com/docs/request-authentication/jwt-templates#default-claims), so that would get me what I think I want/need. I'm still curious to understand why Convex suggests using the tokenIdentifier over the sub. Is tokenIdentifier a standard OpenID Connect (or some other authority's/spec's) concept? Is its format standardized?
Michal Srb
Michal Srb2y ago
It’s because you might use multiple auth providers at the same time with convex. tokenIdentifier ensures its uniqueness, even if the subject was the same across multiple auth providers. It’s a convex thing, not defined by OpenID.
gautamg
gautamg2y ago
It's arguably overkill on our part as most auth providers (Auth0, Google, etc) insert their own string in the subject to keep it globally unique, but as an extra precaution we add our own namespacing to ensure you can never have two users from different auth providers with clashing identifiers
RJ
RJOP2y ago
Hmm ok, I think I'm understanding better. So it sounds like this uniqueness concern would only be an issue if I were not using Clerk (or Auth0) exclusively, and wanted to instead integrate directly with multiple different auth providers who each use e.g. same-formatted UUIDs. This would make collision technically possible if just using the subject, but not if using Convex's tokenIdentifier. Does that sound right?
Michal Srb
Michal Srb2y ago
That’s correct 👍
RJ
RJOP2y ago
Perfect, thank you! 🙂
ian
ian2y ago
As an alternative to passing the user.id in another Clerk claim field, you can pass any extra Clerk-specific fields up when you do storeUser, and associate them with the user at that point, and just use the tokenIdentifier as a lookup to the user object
RJ
RJOP2y ago
The reason I was asking about this in the first place is because I was considering using webhooks to store users instead of a mutation in the client. There are a variety of reasons that seemed attractive to me: 1. The storeUser mutation wasn't working out of the box for me and I didn't want to debug it 2. It would in theory be nice to be able to delete users, especially while testing, and have that propagate easily from Clerk to Convex 3. I'll likely eventually want to wire up webhooks from Clerk anyways for things like sending welcome emails When using a webhook, I get a payload with the Clerk user ID but I don't get any JWT issuer data I did end up going that route and it was very easy to set up
Convex Bot
Convex Bot2y ago
@ian: Makes sense. I'll investigate the storeUser bit. And to confirm, is doing a storeUser(clerkId) better or worse than doing the clerk claim bit? My main concern is that the webhooks are eventually consistent, so the clerk user might not be synced yet. So maybe it'd make sense for storeUser to be an action that hit Clerk synchronously? Thinking out loud
RJ
RJOP2y ago
Ian? Is that you? It gets the tone kinda right
ian
ian2y ago
Haha yeah it's me, I've been building a discord / slack bridge so I can see messages in slack and reply from there, but I haven't gotten it to reply as me
RJ
RJOP2y ago
Nice! It sounded like you because it was you!
And to confirm, is doing a storeUser(clerkId) better or worse than doing the clerk claim bit?
Can you elaborate on what you mean by this?
My main concern is that the webhooks are eventually consistent, so the clerk user might not be synced yet.
Yeah definitely, that's a downside—we'll see how much it matters in practice.
ian
ian2y ago
If you were to get the clerk ID on the client, and call storeUser(clerkId) which would do something like: 1. look up existing user by clerkId 2. Create with db.insert("users", {tokenId, tokenIdentifier})or update user with db.patch(userId, { tokenIdentifier}) In parallel, Clerk's webhook calls to create / update a user: 1. look up existing user by clerkId 2. create or update user with fields from Clerk After both of these happen, the clerk user is fully created & synced with the tokenIdentifier. and thanks to transactions & OCC, they won't overwrite each other. One extension would be to get the initial clerk user fields on the client and pass them up alongside the clerkId so it's all created at once in storeUser (and Clerk webhook would just be for updates)
RJ
RJOP2y ago
I like that strategy! I'll have to look again into why the version of storeUser in the docs wasn't working for me at some point I see that subject is available now on the UserIdentity object, thank you! But it's presence doesn't seem to be reflected in the UserIdentity type (as of Convex 0.12.4, at least).
Michal Srb
Michal Srb2y ago
Correct! The fixed types will go out in the next release (probably next week).
ian
ian2y ago
UserIdentity subject is now in the types: https://blog.convex.dev/announcing-convex-0-14-0/
Convex News
Announcing Convex 0.14.0
Meet the brand new Convex Rust client! We’ve also open sourced the JS client, added schema validation, and more. 0.14.0 is the best version of Convex yet!
RJ
RJOP2y ago
I've adopted this approach btw, it's working great!

Did you find this page helpful?