Squirble
Squirble2w ago

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
Convex Bot
Convex Bot2w ago
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!
erquhart
erquhart2w ago
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.
ballingt
ballingt7d ago
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
Squirble
SquirbleOP7d ago
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?
erquhart
erquhart7d ago
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.
Squirble
SquirbleOP7d ago
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?
erquhart
erquhart7d ago
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
Squirble
SquirbleOP7d ago
Why would I use oauth if I already have the user's information? All I need to do is convince convex that I do
erquhart
erquhart7d ago
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 though
Squirble
SquirbleOP7d ago
Looks like on the client side I want to do something like:
const convex = useConvex()
const validateUser = useAction(api.telegram.actions.validateUser);
useEffect(() => {
convex.setAuth(async () => {
return await validateUser({ initData: Telegram.initData });
});
}, [convex, validateUser]);
const convex = useConvex()
const validateUser = useAction(api.telegram.actions.validateUser);
useEffect(() => {
convex.setAuth(async () => {
return await validateUser({ initData: Telegram.initData });
});
}, [convex, validateUser]);
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?
erquhart
erquhart7d ago
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?
Squirble
SquirbleOP7d ago
it's a hash of all the user info provided plus the secret
erquhart
erquhart7d ago
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
Squirble
SquirbleOP7d ago
it contains an auth_date yes
erquhart
erquhart7d ago
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
Squirble
SquirbleOP7d ago
can I do a better integration with convex's auth system? like, just make a convex action that acts as an auth provider?
erquhart
erquhart7d ago
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
erquhart
erquhart7d ago
I don't believe there's a workaround for working with Convex's actual auth support beyond making a custom provider.
Squirble
SquirbleOP7d ago
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.
import React, { useCallback, useMemo, useState } from "react";
import ReactDOM from "react-dom/client";
import { ThemeProvider } from "next-themes";
import {
ConvexProviderWithAuth,
ConvexReactClient,
} from "convex/react";
import App from "./App.tsx";
import "./index.css";
import { BrowserRouter, Route, Routes } from "react-router";
import TelegramRoutes from "./telegram/routes.tsx";
import { api } from "@convex/_generated/api.js";
import Telegram from "@twa-dev/sdk";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

function useMyAuth() {
console.log("useMyAuth");
const [isLoading, setIsLoading] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const getToken = useCallback(async () => {
console.log("getToken");
setIsLoading(true);
try {
const token = await convex.action(api.telegram.actions.validateUser, {
initData: Telegram.initData,
});
setIsAuthenticated(true);
setIsLoading(false);
return token;
} catch (error) {
setIsAuthenticated(false);
setIsLoading(false);
return null;
}
}, []);
const fetchAccessToken = useCallback(
// @ts-expect-error not using this yet
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
debugger;
console.log("fetchAccessToken");
return await getToken();
},
[getToken],
);
return useMemo(
() => ({
isLoading,
isAuthenticated,
fetchAccessToken,
}),
[isLoading, isAuthenticated, fetchAccessToken],
);
}

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider attribute="class">
<ConvexProviderWithAuth client={convex} useAuth={useMyAuth}>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
{TelegramRoutes}
</Routes>
</BrowserRouter>
</ConvexProviderWithAuth>
</ThemeProvider>
</React.StrictMode>,
);
import React, { useCallback, useMemo, useState } from "react";
import ReactDOM from "react-dom/client";
import { ThemeProvider } from "next-themes";
import {
ConvexProviderWithAuth,
ConvexReactClient,
} from "convex/react";
import App from "./App.tsx";
import "./index.css";
import { BrowserRouter, Route, Routes } from "react-router";
import TelegramRoutes from "./telegram/routes.tsx";
import { api } from "@convex/_generated/api.js";
import Telegram from "@twa-dev/sdk";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

function useMyAuth() {
console.log("useMyAuth");
const [isLoading, setIsLoading] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const getToken = useCallback(async () => {
console.log("getToken");
setIsLoading(true);
try {
const token = await convex.action(api.telegram.actions.validateUser, {
initData: Telegram.initData,
});
setIsAuthenticated(true);
setIsLoading(false);
return token;
} catch (error) {
setIsAuthenticated(false);
setIsLoading(false);
return null;
}
}, []);
const fetchAccessToken = useCallback(
// @ts-expect-error not using this yet
async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
debugger;
console.log("fetchAccessToken");
return await getToken();
},
[getToken],
);
return useMemo(
() => ({
isLoading,
isAuthenticated,
fetchAccessToken,
}),
[isLoading, isAuthenticated, fetchAccessToken],
);
}

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider attribute="class">
<ConvexProviderWithAuth client={convex} useAuth={useMyAuth}>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
{TelegramRoutes}
</Routes>
</BrowserRouter>
</ConvexProviderWithAuth>
</ThemeProvider>
</React.StrictMode>,
);
erquhart
erquhart7d ago
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.
Squirble
SquirbleOP7d ago
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.
erquhart
erquhart7d ago
No, there’s not Yeah building auth is a whole thing for sure
Squirble
SquirbleOP7d ago
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?
erquhart
erquhart7d ago
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.
erquhart
erquhart7d ago
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.
erquhart
erquhart7d ago
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.
erquhart
erquhart7d ago
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.
ballingt
ballingt7d ago
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.
erquhart
erquhart7d ago
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
ballingt
ballingt7d ago
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
erquhart
erquhart7d ago
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
ballingt
ballingt7d ago
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
erquhart
erquhart7d ago
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
ballingt
ballingt7d ago
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!
ampp
ampp7d ago
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.
Squirble
SquirbleOP6d ago
GitHub
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.
Squirble
SquirbleOP6d ago
Is it possible to do something like
crypto.createHmac("sha256", key).update(msg).digest();
}
crypto.createHmac("sha256", key).update(msg).digest();
}
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?
ballingt
ballingt6d ago
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 work
Squirble
SquirbleOP6d ago
So 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.
erquhart
erquhart6d ago
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.
ballingt
ballingt6d ago
Yeah explicit, unless you write a custom useQuery hook that passes it to every function. But I'd probably leave it explicit.
Squirble
SquirbleOP6d ago
even if I wrote a custom useQuery hook I'd still not be able to use convex.auth.getUserIdentity on the server side, right?
ballingt
ballingt6d ago
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 you
Squirble
SquirbleOP6d ago
Can I generate a JWT that clerk will believe is valid? It seems I can set a custom secret in clerk....
ballingt
ballingt6d ago
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)
erquhart
erquhart6d ago
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
Squirble
SquirbleOP6d ago
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?
erquhart
erquhart6d ago
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
ballingt
ballingt6d ago
Ah sorry I keep jumping in without reading the thread, but using Clerk sounds like a good idea too
erquhart
erquhart6d ago
lol so many solutions
Squirble
SquirbleOP6d ago
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...
erquhart
erquhart6d ago
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
Squirble
SquirbleOP6d ago
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.
erquhart
erquhart6d ago
They have a discord, maybe check there
Squirble
SquirbleOP6d ago
yeah I posted the question So how do I build this custom token sign in flow with just react and convex?
erquhart
erquhart6d ago
That's what the guide tells you
Squirble
SquirbleOP6d ago
Ah I guess it doesn't really require nextjs does it? looks like a regular react component to me
erquhart
erquhart6d ago
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
Squirble
SquirbleOP6d ago
er, I guess this part is nextjs specific and needs to be something else:
import { useUser, useSignIn } from '@clerk/nextjs'
import { useUser, useSignIn } from '@clerk/nextjs'
ah I see, they are also available in @clerk/clerk-react It's working =]
ampp
ampp6d ago
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?
Squirble
SquirbleOP6d ago
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...