Telegram Authentication
I'm writing a React app with Convex.
I want to support Telegram's super simple Mini App authentication.
Here's how it works:
My telegram bot shares a special link. When a user clicks on the link, it opens a web view to my app, but with some additional information in the fragment identifier of the URL: it passes the telegram user's id, username, first and last name, photo url, a timestamp, and a hash of all that information with the bot's secret.
I've put the bot's secret in my convex environment variables.
I've also written a Convex action that verifies the user information against that secret. This action uses node, so I can't easily re-verify it in every query.
How can I get Convex to see the user as authenticated? Perhaps I need to have my action return a JWT that Convex understands? How do I get the convex client in the web browser to use that JWT in subsequent requests so the user shows up as authenticated in later queries?
61 Replies
Thanks for posting in <#1088161997662724167>.
Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets.
- Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.)
- Use search.convex.dev to search Docs, Stack, and Discord all at once.
- Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI.
- Avoid tagging staff unless specifically instructed.
Thank you!
Anything that integrates with your Convex app over http actions will generally involve some kind of auth workaround. Any method that you’re confident in for verifying the user will work, you’ll just need to have the bot only use Convex functions that don’t require actual authentication.
In a webhook scenario, once I verify a webhook through whatever method the webhook sender supports, I treat the request as authenticated on behalf of the related user, if any. Within the scope of the webhook’s purpose, of course.
Regarding the Clerk question in your original message in #general , Clerk is a great auth option, but there isn’t one specific one that Convex recommends. Convex has first class integrations for Clerk, Auth0, and of course, Convex Auth.
Telegram purports to have OAuth support, so although it's not one of the officially supported providers for Convex Auth and it's not some of the ~100 that auth core supports https://authjs.dev/getting-started/authentication/oauth
you may be able to write your own provider based on those examples
I'm not sure you're understanding my question?
Here's the workflow:
I open my web app inside telegram. It's just a regular embedded browser, and it is opened to a URL that has user data in the fragment identifier. The data includes a hash that's derived from a secret only the server knows (it's in an environment variable). So, I can send the user data to a convex action to verify its integrity. After that, I'd like to set some kind of token that the web browser can use for future requests that convex will understand. A JWT I guess?
Technically I could pass the initial user data with every action, that would be inconvenient because I'm using node's crypto library to verify the hash, and not all convex actions allow node.
Essentially my question is, once a convex action knows who the user is, how do they send an auth token so future calls from the same browser can trust them as well?
I think we’re on the same page, your telegram scenario differs from webhooks in that it has a client, but otherwise they’re the same. Sending the hashed token with every request is fine, just make a node action that your HTTP action calls to verify the token each time.
Tom’s suggestion sounds even better, worth looking into oauth if they support it.
But looking at your last comment in general about skipping to the end, I’d just send the token with every request.
How do I get a token?
I can't find any public API in convex that gives a token that works with convex auth
Once my authentication is done can't I create some kind of JWT that makes convex see the user as authenticated the same way it would with clerk or convex-auth?
I really don't think you understand my situation. This is really just a regular react app. It happens to already know the user's information. It calls a regular convex action to verify that with a secret I have in an environment variable. After that I want it to act like a normal authenticated user as far as convex is concerned. So how do I integrate with convex's idea of authentication to tell it who the user is and that they're already logged in?
Ah so this is a Convex app, not just using http actions for requests. Got it
I’d echo Tom then, would at least take a look at their oauth support
Why would I use oauth if I already have the user's information?
All I need to do is convince convex that I do
You either have to use real auth, which means either a supported integration or a custom integration with a compatible identity provider, or you work around real auth, which is more what you're trying to do. You're not locked in to Convex's auth model to authorize requests, you just can't use
ctx.auth
like you would with actually authenticated requests.
I would run it auth-less if I were you.
Does take some thought since you're trying to actually run convex client and not just make requests thoughLooks like on the client side I want to do something like:
And then I need to make my convex action return a JWT. Guess I'm on my own on generating that huh?
No way to hook in to convex's built in libraries for generating JWTs?
Not that I'm aware of. But you're also getting close to building a custom auth system here lol
The data includes a hash that's derived from a secret only the server knows (it's in an environment variable). So, I can send the user data to a convex action to verify its integrity.Is the hash tied to the user in any way?
it's a hash of all the user info provided plus the secret
Is there an expiration?
I'm wondering if this is maybe suitable to pass with each request as long as you're only storing it in memory
Ah but that limits you to only using actions
This is terrible, but if there's an expiration that can be verified, maybe you verify once and then basically make your session from that, store it in a table, reference in requests for that session.
You could still pass the hash plus the session id, session table would store the expiration, then you can verify each call without calling an action
very dubious security footing here lol. but maybe works
it contains an auth_date yes
you wouldn't really have a way to refresh, maybe they just get kicked out after a certain point
unless there's a refresh button in the telegram ui surrounding your app, maybe you signal the user to refresh on expiration
can I do a better integration with convex's auth system?
like, just make a convex action that acts as an auth provider?
There isn't really a great in-between - you can make a custom auth provider, docs for that here: https://docs.convex.dev/auth/advanced/custom-auth
Custom Auth Integration | Convex Developer Hub
Note: This is an advanced feature! We recommend sticking with the
I don't believe there's a workaround for working with Convex's actual auth support beyond making a custom provider.
So, do I have to do both the server side and client side integration?
Can I implement my auth provider in the same convex instance?
Hm. I'm not understanding this. How do you actually trigger that custom authentication?
I tried this but it doesn't seem to be calling
fetchAccessToken
and I'm not sure how to make that happen.
Convex can be integrated with any identity provider supporting the OpenID Connect protocol. At minimum this means that the provider can issue ID tokens and exposes the corresponding JWKS. The ID token is passed from the client to your Convex backend which ensures that the token is valid and enables you to query the user information embedded in the token, as described in Auth in Functions.It is very much non trivial, you would be creating a complete authentication provider If you really want true auth for this, figuring out how to get Telegram’s oauth support working as Tom mentioned would probably be your best bet.
There's no way to just make a JWT in convex and tell convex to trust it?
I managed to craft a convincing enough JWT but now it's trying to hit
/.well-known/openid-configuration
expecting a full on openid implementation. Bleh.No, there’s not
Yeah building auth is a whole thing for sure
This is pretty ridiculous. In most servers I'd just be able to create a cookie for this.
How does convex work for password-based authentication then?
Would it be possible to implement your own password-based authentication?
Everything you can do with auth in Convex is covered here: https://docs.convex.dev/auth
Authentication | Convex Developer Hub
Add authentication to your Convex app.
Password, oauth, any type of auth, is going to go through a provider like Clerk or Auth0, Convex's own Auth (which uses Auth.js), or a custom integration with an OpenID Connect compatible provider. Cookies aren't directly supported in Convex yet.
Wondering if maybe Clerk has the api's you need to facilitate your token swap purely on the backend.
Custom Flows: Embeddable email links with sign-in tokens
Learn how to build custom embeddable email link sign-in flows to increase user engagement and reduce drop off in transactional emails, SMS's, and more.
The example here is for email, but folks in their Discord are using it for embedded apps, Electron, etc. You can generate a "sign-in token" on the fly and then use it to authenticate in the client.
I'm not sure if this is clear already from this thread, but you can avoid using the JWT stuff for Convex if you can verify tht auth info inside a mutation, query, or action (so without making an HTTP request)
If your Convex backend has the secret to decode this then that's enough to prove identity. You're rolling your own auth at this point, so there's some security considerations. You won't be using
ctx.auth.getIdentityToken()
as that's just for JWTs that can hit an openid-configuraiton endpoint etc., but if you're fine with the presence of that user information encrypted with a secret being enough to prove identity you don't need any of that.I think the challenge is they would need to send the encrypted hash with every request, so they can only use actions as a query/mutation can't call an action and get a result
Unless I'm missing something in the recommendation here. A workaround that doesn't require adding Clerk would be cool
So you can get the information from another server? If it's all there then you just need to decrypt that data
it'd be a hassle for queries where the auth info could expire though
But they'd have to decrypt on every call (with node) unless they made a hacky "session" table, right?
Only options I see are stateless, where the hash is decrypted every call, or stateful, which requires storing sessions in the db
oh I missed that part, does their decryption algorithm only run in Node.js?
this doesn't help them now but we'd love to expand the Convex JavaScript runtime with crypto algorithms we're missing today, unless this actually requires a network request then it's a bummer to need to retreat to an action just to decrypt something
yeah one of the earlier messages says they're using node crypto
being able to decrypt in convex runtime would be the ticket for sure
got it, ok I'm jumping in without context here. If there's an algorithm in particular we could add support for we'd love to hear about it!
Basically, I equate what you are talking about as similar to the session tracking via client side sessionId storage helper (part of convex helpers), so instead of passing a session its passing a custom auth token to a custom context that handles the queries/mutations. With all the risks that that entails.
https://github.com/grammyjs/validator?tab=readme-ov-file#web-bots-validation
This package implements the verification in
validateWebAppData
. It's not terribly complicated: https://github.com/grammyjs/validator/blob/main/src/mod.ts#L13-L28 but it does use sha256
https://github.com/grammyjs/validator/blob/main/src/deps.node.tsGitHub
validator/src/deps.node.ts at main · grammyjs/validator
Validation logic for Web Apps and login widgets. Contribute to grammyjs/validator development by creating an account on GitHub.
GitHub
GitHub - grammyjs/validator: Validation logic for Web Apps and logi...
Validation logic for Web Apps and login widgets. Contribute to grammyjs/validator development by creating an account on GitHub.
GitHub
validator/src/mod.ts at main · grammyjs/validator
Validation logic for Web Apps and login widgets. Contribute to grammyjs/validator development by creating an account on GitHub.
Is it possible to do something like without
"use node"
? And if so, how would I get convex to pass my telegram init data with every action/mutation/etc or would I have to do that manually?await crypto.subtle.digest('SHA-256', msgBuffer)
should work
instead of Node.js import crypto
it's a global variable crypto.subtle
https://developer.mozilla.org/en-US/docs/Web/API/Crypto/subtle like a browser
you could pass that manually or write a custom useQuery
, useMutation
etc. hooks that add it for you
I'd start with passing it manually
the await crypto.subtle.digest
is not an exact replacement, there's some finicky types stuff to do, ArrayBuffers vs strings etc.
LLMs are good at this, I'd add "in a browser javascript runtime, not Node.js" to get an answer that will workSo this would have to be an explicit argument in every single action/query/mutation?
I'll even have to pass it and re-verify if an action calls other queries or mutations.
Definitely not as convenient as just checking
convex.auth.getUserIdentity
. Is there no way to hook into that?
Honestly considering just writing an OpenID endpoint to make convex happy so I can do the auth in one place and then not have to think about it in the rest of the app.Yeah this would be outside of standard auth. If you really want to make proper auth work today given the requirements of your Telegram app, I think your best bet is the Clerk approach I mentioned above. Should work the way you're looking for, as they support effectively overriding auth with a token, and Convex integrates with Clerk.
Would be far simpler than rolling your own custom auth integration
The simplest way, though, is what Tom's suggesting. You'd send the hash with each request, but it works. But yeah, for full auth, go with Clerk.
Yeah explicit, unless you write a custom useQuery hook that passes it to every function. But I'd probably leave it explicit.
even if I wrote a custom
useQuery
hook I'd still not be able to use convex.auth.getUserIdentity
on the server side, right?that's right,
convex.auth.getUserIdentity
is not programmable like this
You can always replace it with your own thing with custom functions or your own helper function without custom functions
today ctx.auth.getUserIdentity
always returns claims from a JWT that the Convex backend verifies for youCan I generate a JWT that clerk will believe is valid? It seems I can set a custom secret in clerk....
Clerk I don't know about, probably
for Convex yes, by implementing the OAuth spec (so responding to that .well-known endpoint that Convex tries to hit)
I'm probably muddying the waters here lol, but I can advise on the clerk approach if that's desired. Custom auth is starting to sound simpler than I thought though, so maybe Clerk isn't necessary
Well for OpenID Connect I'd just have to implement those two http actions:
/.well-known/openid-configuration
and /.well-known/jwks.json
right?
The Clerk solution you're describing is kinda stateful, right? I assume it would cause Clerk to temporarily store some information to represent the login token you generated?As I understand it, you're getting the same kind of session you get through a standard login
Not quite sure what happens at refresh time or what the expiration is
Ah sorry I keep jumping in without reading the thread, but using Clerk sounds like a good idea too
lol so many solutions
I wanna try just generating a clerk-compatible JWT. I generated a custom signing key pem but convex doesn't seem to preserve newlines when i paste it in...
If you want to make your own jwt I would definitely leave Clerk out of it and go with Tom's suggestion
The clerk endpoint I linked to above generates a jwt, is that what you're looking for?
You shouldn't need to generate a custom signing key. You'd follow the Convex docs for integrating with clerk: https://docs.convex.dev/auth/clerk
then generate a token with Clerk's backend api: https://clerk.com/docs/reference/backend-api/tag/Sign-in-Tokens#operation/CreateSignInToken
Then you'd use the signin approach here (except drop the next stuff if you're not using next): https://clerk.com/docs/custom-flows/embedded-email-links#build-a-custom-flow-for-signing-in-with-a-sign-in-token
Man, I really think I can make this work where I just forge a clerk jwt essentially, since it lets you provide your own signing key. Perhaps I'm not putting in my signing key correctly though because it's breaking its normal operation. Don't see docs on what format the signing key is supposed to be inputted.
They have a discord, maybe check there
yeah I posted the question
So how do I build this custom token sign in flow with just react and convex?
That's what the guide tells you
Ah I guess it doesn't really require nextjs does it?
looks like a regular react component to me
no, it just said it was for next so i mentioned to ignore any next stuff
the import is next specific, but there's a react version
er, I guess this part is nextjs specific and needs to be something else:
ah I see, they are also available in
@clerk/clerk-react
It's working =]Nice, I'm curious are you using this to just verify the ownership of the telegram account so its like a private message or just to make it easy to login?
just to identify the user instantly. I'm making a poll app so I want each user to get one vote.
Integrating with clerk means perhaps I can use other types of auth later, since I want to integrate with other types of messengers that maybe clerk will actually help with :P. Unfortunately Telegram doesn't give any sort of universal identifier like email or phone number though so idk how I'd detect duplicate users authenticating in two different ways unless the user intentionally merges theirself shrug but I guess the poll runners could decide which auths they want to allow.
Btw, it only works if I set some kind of identifier on the user, like a username, email, or phone number. Since I don't have any of that, I'm using the convex ID for my local user row as the clerk username. Kind of a hack working around that limitation...