Telegram authentication
tg-auth
From a Telegram launch to a reliable application session.
tg-auth-publicI maintain the authentication package used by Pebbler for Telegram and its backend services. It connects Telegram Mini App launch data with server verification, encrypted application credentials, and the browser session lifecycle.
The public edition brings that shared browser and server code into a standalone, documented package. Its scope is identity verification and credential management; account lookup, permissions, and business rules remain with the application.
tg-authTelegram → authenticated sessionMaking the server the source of trust
The browser submits Telegram’s raw initData. A user object read from the browser, including initDataUnsafe, is not sufficient evidence of identity. The server verifies the protocol proof before an application can use the resulting identity to find an account.
The verifier supports bot-token HMAC and Telegram’s Ed25519 signatures. Parsing matters as much as the cryptographic check: duplicate decoded keys, malformed encoding, and ambiguous fields are rejected. Freshness checks bound the lifetime of launch data, with a small allowance for clock differences.
const launch = await auth.verifyInitData(rawInitData);
if (!launch.valid || !launch.user) {
throw new Error('Telegram sign-in rejected');
}
const account = await accounts.findAllowedTelegramUser(launch.user.id);
const credentials = await auth.genAccessToken({
userId: account.id,
telegramId: String(launch.user.id),
});Illustrative integration excerpt. The configured verifier supplies the trusted Telegram identity; the application supplies account lookup and its claim schema.
Server integration referenceBalancing repeated launches and replay protection
Verified launch data is a bearer assertion: possession of a valid assertion is enough to present it again while it remains fresh. Verification alone does not make it single-use. Telegram can also supply the same assertion more than once during a launch, so rejecting every repeated exchange would affect legitimate resume behavior.
Single-use verification is therefore an explicit option. When enabled, an atomic store consumes a fingerprint of the authenticated fields. Equivalent query encodings and different supported proof paths map to the same fingerprint. A repeated launch resumes through its refresh cookie rather than exchanging the same assertion again. The application chooses its freshness window and replay policy for the experience it supports.
Keeping refresh and logout predictable
Several API requests can discover an expired access token at the same time. The browser client shares one pending refresh request instead of creating a separate exchange for each caller. Valid access credentials stay in memory by default; the refresh credential stays in an HttpOnly cookie.
Another challenge is a response arriving after logout. Each credential operation belongs to a session generation. Signing out, resetting, or reinitializing the client invalidates pending commits, so a delayed response cannot restore the old local session. This protects browser state; the backend still owns session closure and token verification.
import { TelegramAuthClient } from '@bridge-applications/tg-auth/client';
const session = await TelegramAuthClient.getAccessTokenSilently();
const response = await fetch('/api/account', {
headers: { Authorization: `Bearer ${session.accessToken}` },
});After client initialization and session bootstrap, concurrent callers reuse a valid credential or share the pending refresh request.
Browser session referenceKeeping credentials within their intended scope
Access and refresh tokens use encrypted JWE envelopes with different purposes and scopes for the application, bot, and environment. Verification checks those boundaries as well as expiry and the configured claim schema. A refresh credential cannot serve as an access credential, and a token from one application context cannot silently become valid in another.
Shared refresh-group counters support account-wide invalidation across backend instances. Incrementing a group invalidates the existing refresh credentials for that identity. Already issued access tokens retain their original expiry, so their short lifetime bounds the remaining access window. The group counter must be durable: deleting it can restore authority to an older group-zero credential.
From internal reuse to a public package
The internal package is used by Pebbler for Telegram. The public edition documents integration responsibilities and provides separate client, server, and schema entry points. Browser consumers can import the client without bringing server cryptography or Redis dependencies into their bundle.
The repository’s checks cover cryptographic verification, HTTP integration, asynchronous session races, packed-package imports and types, browser dependency isolation, and actual workerd cryptography. These checks make the package’s behavior inspectable; each consuming application still needs to verify its deployed cookies, routes, authorization, and session policy.