8.1 KiB
PRD: Telegram Phone-Number Authentication
Problem
The current Telegram integration treats Telegram as a secondary identity layer: a user must already hold an Amanat email-password (or Google OAuth) account before they can link their Telegram identity. This creates unnecessary friction for users who arrive from Telegram, have never heard of Amanat, and reasonably expect the Mini App to just work — as every other well-built Telegram Mini App does.
Telegram accounts are phone-number-verified by Telegram itself. When the backend successfully verifies Mini App initData, it has already established that the requester holds a specific Telegram account tied to a phone number. That is sufficient identity to create and authenticate an Amanat account without asking for an email or password.
Goal
Allow users to sign in to Amanat using their Telegram identity as the primary credential — with zero separate signup step. This applies to:
- Telegram Mini App context —
initDatais auto-available and already cryptographically verified. - Telegram Login Widget — the standard web widget that lets users authenticate via Telegram on any browser page; returns a signed payload verifiable against the bot token.
In both cases the user's phone-verification is owned by Telegram; Amanat trusts the signed assertion and does not re-verify the phone number itself.
Platform assumptions
- Telegram Mini App
initDatais signed with HMAC-SHA-256 keyed onHMAC-SHA-256("WebAppData", BOT_TOKEN). Server-side verification is already implemented intelegramService.ts. - Telegram Login Widget returns
{id, first_name, last_name, username, photo_url, auth_date, hash}. Hash isHMAC-SHA-256(data_check_string, SHA256(BOT_TOKEN)). The same bot token covers both flows. - Telegram user IDs are stable, globally unique integers that do not change even if the user changes username or phone number.
- Telegram does not expose the raw phone number to third-party apps — the ID is the stable identity anchor.
Reference docs:
- Telegram Login Widget: https://core.telegram.org/widgets/login
- Mini Apps initData: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
What changes
Backend
New endpoint: POST /auth/telegram
Accepts one of:
{ initData: string }— from Mini App context{ id, first_name, last_name, username, photo_url, auth_date, hash }— from Login Widget
Steps:
- Verify signature (reuse
verifyMiniAppInitDatafor initData path; addverifyTelegramLoginWidgetfor widget path). - Extract
telegramUserId(stable integer). - Look up
TelegramLinkbytelegramUserId. - If link found and active → load the linked Amanat user → issue JWT + refresh token (same format as
POST /auth/login). - If no link found → auto-provision a new Amanat user:
email:null(nullable; add index exclusion for null values)firstName/lastName: from Telegram profileusername:tg_<telegramUserId>as stable internal handlerole:buyer(default; user can change)isEmailVerified:false— set a flagtelegramVerified: trueinsteadstatus:activeauthProvider:telegram- Create
TelegramLinkrecord in the same transaction. - Issue JWT + refresh token.
- On success, always upsert
TelegramLink.lastSeenAtand update name/username fields from latest Telegram profile. - Return:
{ token, refreshToken, user, isNewUser: boolean }—isNewUser: truesignals the frontend to show an onboarding nudge (optional email capture, preferred language, currency).
User model changes:
email: make nullable (type: String, sparse: true— allows multiple null values in a unique sparse index)- Add
telegramVerified: Boolean(defaultfalse) - Add
authProvider: 'email' | 'google' | 'telegram'field - Existing email-based users are unaffected; their
authProviderdefaults to'email'
Rate limiting and security:
- Apply the same replay protection already in
telegramService.tsto this endpoint. - Rate limit: 10 requests per IP per minute, 5 per Telegram user ID per minute.
- Log all auto-provisioning events as audit records.
- Reject
auth_dateolder thanTELEGRAM_MINIAPP_MAX_AGE_MS(already configurable).
Blocked account handling:
- If
TelegramLink.status === 'blocked'→ return 403 withACCOUNT_BLOCKEDcode. - If the linked Amanat user is suspended/deleted → return 403 with
ACCOUNT_SUSPENDED.
Frontend
Mini App auto-login (inside Telegram):
When window.Telegram?.WebApp?.initData is non-empty:
- Skip the login page entirely.
- POST
initDatato/auth/telegram. - Store the returned JWT in the existing auth context.
- If
isNewUser === true, show a lightweight onboarding overlay inside the Mini App to capture optional email and preferred settings before routing to the main app.
Web login page — "Continue with Telegram" button:
- Add a "Continue with Telegram" button alongside the existing Google button.
- Clicking it opens the Telegram Login Widget in a popup (
window.openor inline script tag method). - On callback, POST the widget payload to
/auth/telegram. - This works on any browser, not only inside Telegram.
Auth types update:
// auth/types.ts additions
export interface User {
// ... existing fields ...
telegramVerified: boolean;
authProvider: 'email' | 'google' | 'telegram';
telegramUsername?: string;
}
Onboarding nudge (post-Telegram-auth):
New users created via Telegram auth should be shown a non-blocking screen:
- Optional: "Add an email for account recovery" (not required)
- Optional: preferred language and currency
- Skippable — routing to the main app without completing it is fine
Non-goals
- Do not expose the user's raw phone number.
- Do not require email for Telegram-authenticated users.
- Do not use Telegram auth as a bypass for high-risk actions (release, payout address change, dispute resolution still require step-up confirmation as per task 5.6/5.8 policy).
- Do not auto-merge a Telegram-provisioned account with an existing email account unless the user explicitly initiates account linking from settings.
Account merge and collision handling
| Scenario | Behavior |
|---|---|
| Telegram user arrives, no existing account | Auto-provision, create TelegramLink, return isNewUser: true |
| Telegram user arrives, TelegramLink already exists | Auth as linked user, update last-seen |
| Email user opens Mini App, not yet linked | Show "Link your Telegram" prompt (existing task 5.2 flow) — do NOT auto-merge |
| Two Telegram accounts try to link to same email user | Reject second; return 409 DUPLICATE_TELEGRAM_LINK |
| Same Telegram user tries to auth with two different app accounts | Impossible by design — TelegramLink.telegramUserId is unique |
Acceptance criteria
- A new Telegram user can complete authentication inside the Mini App without entering an email or password.
- A returning Telegram user gets the same JWT session whether they authenticate via Mini App
initDataor the web Login Widget. POST /auth/telegramrejects replayedinitDatawithin the existing replay window.POST /auth/telegramrejectsauth_dateolder than the configured max-age.- Blocked Telegram accounts receive a 403 response; they cannot circumvent it by unlinking and re-linking.
- Auto-provisioned users have
authProvider: 'telegram'and no email; existing email users are unaffected. - Admin UI (task 5.7) can distinguish
authProvider: telegramusers and showstelegramVerifiedstatus. - A Telegram-authed user who later adds an email can then also log in via email — both paths converge to the same user record.
- High-risk actions still require the step-up policy regardless of auth provider.
- The "Continue with Telegram" button is visible on the web login page in non-Mini-App contexts.
Dependencies
- Task 5.2 (identity linking model) — TelegramLink model and verifyMiniAppInitData are already done; this task extends the auth path, not the linking model.
- Task 5.8 (security controls) — replay protection, rate limits, and audit logging from that task apply here too.
- Task 5.6 (high-risk action policy) — must not be weakened.