# auth.aipika.tech — Sign in with Telegram — integration spec (for LLMs/agents) You are integrating a website ("the client") so users log in via Telegram. Follow this spec literally. Default mode needs NO registration. ## FACTS - PROVIDER_BASE = https://auth.aipika.tech - Model: OAuth2-style Authorization Code. The provider verifies the user via a Telegram Mini App; you get a one-time `code` and exchange it server-side for the user's Telegram profile. - IDENTITY MODEL: your `redirect_uri`'s ORIGIN is your identity. The result is bound to that origin via `aud`. There is NO registration by default and no client_id/secret. YOU set your own session on your own domain. ## WHAT YOU IMPLEMENT (two routes) 1. `GET /login`: redirect browser to PROVIDER_BASE/authorize. 2. `GET `: exchange `code` at PROVIDER_BASE/token, VERIFY `aud`, then create the client's own session. ## ENDPOINT: GET {PROVIDER_BASE}/authorize (browser navigates here) Query params: - redirect_uri (REQUIRED; must be https, or http on localhost for dev) — this is your identity - state (recommended; opaque CSRF value you generate) - scope (optional) - code_challenge, code_challenge_method ("S256"|"plain") (optional PKCE, see HARDENINGS) - client_id (optional; only if registered as a confidential client) On success → 302 to: {redirect_uri}?code={ONE_TIME_CODE}&state={state} ## ENDPOINT: POST {PROVIDER_BASE}/token (server-to-server) Request JSON body: { "code": "" } optional: "code_verifier" (PKCE), "client_id"+"client_secret" (confidential) Response 200 JSON: { "token_type": "Bearer", "aud": "https://yourapp.com", // <-- the origin this was minted for "sub": "", "telegram_id": "", "first_name": "string", "last_name": "string|null", "username": "string|null", "photo_url": "string|null" // "id_token" present ONLY in confidential mode } Errors: 400 invalid_request | 400 invalid_grant (code expired>120s / reused / PKCE bad) | 401 invalid_client (confidential: bad/missing secret). The `code` is single-use and ~120s; exchange immediately, server-side. ## ABSOLUTE RULE: verify aud After /token, you MUST check `response.aud === ` (scheme+host+port). If it doesn't match, REJECT. This is what prevents accepting a code/identity that was minted for a different site. Skipping this check is a critical bug. ## CROSS-BROWSER RULE The user may land on your CALLBACK_URL in a DIFFERENT browser than the one that started login. That browser has no state cookie from your site. Therefore: - Verify `state` ONLY IF your state cookie is present (same-browser case). - If absent, accept the one-time `code` anyway (it's delivered only into the user's private Telegram chat, single-use, short-lived, aud-bound). - Do NOT hard-require `state`, or cross-browser login breaks. (aud + one-time are the protections instead.) ## REFERENCE IMPLEMENTATION (Node/Express; adapt to any stack) ```js const AUTH = "https://auth.aipika.tech"; const PUBLIC_URL = "https://yourapp.com"; const REDIRECT_URI = PUBLIC_URL + "/auth/telegram/callback"; const OWN_ORIGIN = new URL(PUBLIC_URL).origin; app.get("/login", (req, res) => { const state = crypto.randomBytes(16).toString("base64url"); res.cookie("tg_state", state, { httpOnly: true, sameSite: "lax", secure: true, maxAge: 600000 }); const u = new URL(AUTH + "/authorize"); u.searchParams.set("redirect_uri", REDIRECT_URI); u.searchParams.set("state", state); res.redirect(u.toString()); }); app.get("/auth/telegram/callback", async (req, res) => { const { code, state } = req.query; if (!code) return res.status(400).send("missing code"); const expected = req.cookies?.tg_state; // parse cookies however you do if (expected && state && expected !== state) return res.status(400).send("bad state"); const r = await fetch(AUTH + "/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); if (!r.ok) return res.status(401).send("auth failed"); const t = await r.json(); // { telegram_id, ..., aud } if (t.aud !== OWN_ORIGIN) return res.status(400).send("bad aud"); // REQUIRED // create YOUR session for t.telegram_id (HttpOnly+Secure+SameSite=Lax cookie), then: res.redirect("/"); }); ``` ## OPTIONAL: EMBEDDED MODE (iframe, no domain change) Instead of a top-level redirect, embed the login in an iframe on your page. - Include `` and call: `TelegramLogin.mount("#el", { redirectUri: "", onSuccess: fn })`. - The widget loads `{PROVIDER_BASE}/authorize?...&embed=1` in an iframe; on success the auth page `postMessage`s `{ type:"telegram-auth", code, state }` to your page (targeted at your origin) instead of redirecting. - `onSuccess({code,state})` fires; send `code` to YOUR backend, which does the SAME `/token` exchange + `aud` check + sets your session, returns 200; then reload. - Security: the widget only accepts messages where `event.origin === PROVIDER_BASE` and `state` matches; the auth page sets `CSP: frame-ancestors` to your origin. Clickjacking can't steal identity (result is aud-bound + user must confirm in TG). - Mainly a desktop convenience (QR). On mobile "Open Telegram" opens a new tab. ## OPTIONAL HARDENINGS - PKCE (strict, same-browser): send code_challenge=base64url(sha256(verifier)) + code_challenge_method=S256 at /authorize, and code_verifier at /token. Closes leaked-code replay, but DISABLES cross-browser completion (binds to the initiating browser). Use only when same-browser is acceptable. - Confidential client: register (operator runs `npm run client -- add ...`), then send client_id at /authorize and client_id+client_secret at /token. Enforces a redirect allowlist + secret and returns a verifiable id_token (HS256 with your secret). Cross-browser still works. ## DO / DON'T - DO verify aud === your origin (non-negotiable). - DO keep any client_secret server-side only; exchange code server-side. - DO make `state`/PKCE checks conditional unless you intentionally want strict same-browser. - DON'T call /api/status, /r/:token, /miniapp, /api/miniapp/resolve — internal. - DON'T put the code in client-side JS or trust it without exchanging at /token. ## INTEGRATION CHECKLIST [ ] /login redirects to {PROVIDER_BASE}/authorize with redirect_uri (https) + state [ ] redirect_uri origin == your site's origin [ ] callback exchanges code at /token server-side, handles 400/401 [ ] callback verifies aud === your origin <-- critical [ ] state verified only-if-present; cross-browser code accepted [ ] own session cookie set HttpOnly+Secure+SameSite=Lax [ ] tested from a phone where Telegram opens the Mini App ```