Files
nick-doc/.taskmaster/docs/prd-telegram-phone-auth.md
2026-05-24 16:12:46 +04:00

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:

  1. Telegram Mini App contextinitData is auto-available and already cryptographically verified.
  2. 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 initData is signed with HMAC-SHA-256 keyed on HMAC-SHA-256("WebAppData", BOT_TOKEN). Server-side verification is already implemented in telegramService.ts.
  • Telegram Login Widget returns {id, first_name, last_name, username, photo_url, auth_date, hash}. Hash is HMAC-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:

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:

  1. Verify signature (reuse verifyMiniAppInitData for initData path; add verifyTelegramLoginWidget for widget path).
  2. Extract telegramUserId (stable integer).
  3. Look up TelegramLink by telegramUserId.
  4. If link found and active → load the linked Amanat user → issue JWT + refresh token (same format as POST /auth/login).
  5. If no link found → auto-provision a new Amanat user:
    • email: null (nullable; add index exclusion for null values)
    • firstName / lastName: from Telegram profile
    • username: tg_<telegramUserId> as stable internal handle
    • role: buyer (default; user can change)
    • isEmailVerified: false — set a flag telegramVerified: true instead
    • status: active
    • authProvider: telegram
    • Create TelegramLink record in the same transaction.
    • Issue JWT + refresh token.
  6. On success, always upsert TelegramLink.lastSeenAt and update name/username fields from latest Telegram profile.
  7. Return: { token, refreshToken, user, isNewUser: boolean }isNewUser: true signals 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 (default false)
  • Add authProvider: 'email' | 'google' | 'telegram' field
  • Existing email-based users are unaffected; their authProvider defaults to 'email'

Rate limiting and security:

  • Apply the same replay protection already in telegramService.ts to 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_date older than TELEGRAM_MINIAPP_MAX_AGE_MS (already configurable).

Blocked account handling:

  • If TelegramLink.status === 'blocked' → return 403 with ACCOUNT_BLOCKED code.
  • 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:

  1. Skip the login page entirely.
  2. POST initData to /auth/telegram.
  3. Store the returned JWT in the existing auth context.
  4. 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.open or 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

  1. A new Telegram user can complete authentication inside the Mini App without entering an email or password.
  2. A returning Telegram user gets the same JWT session whether they authenticate via Mini App initData or the web Login Widget.
  3. POST /auth/telegram rejects replayed initData within the existing replay window.
  4. POST /auth/telegram rejects auth_date older than the configured max-age.
  5. Blocked Telegram accounts receive a 403 response; they cannot circumvent it by unlinking and re-linking.
  6. Auto-provisioned users have authProvider: 'telegram' and no email; existing email users are unaffected.
  7. Admin UI (task 5.7) can distinguish authProvider: telegram users and shows telegramVerified status.
  8. A Telegram-authed user who later adds an email can then also log in via email — both paths converge to the same user record.
  9. High-risk actions still require the step-up policy regardless of auth provider.
  10. 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.