auth.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import { Api } from "../tl";
  2. import * as utils from "../Utils";
  3. import { sleep } from "../Helpers";
  4. import { computeCheck as computePasswordSrpCheck } from "../Password";
  5. import type { TelegramClient } from "./TelegramClient";
  6. /**
  7. * For when you want to login as a {@link Api.User}<br/>
  8. * this should handle all needed steps for authorization as a user.<br/>
  9. * to stop the operation at any point just raise and error with the message `AUTH_USER_CANCEL`.
  10. */
  11. export interface UserAuthParams {
  12. /** Either a string or a callback that returns a string for the phone to use to login. */
  13. phoneNumber: string | (() => Promise<string>);
  14. /** callback that should return the login code that telegram sent.<br/>
  15. * has optional bool `isCodeViaApp` param for whether the code was sent through the app (true) or an SMS (false). */
  16. phoneCode: (isCodeViaApp?: boolean) => Promise<string>;
  17. /** optional string or callback that should return the 2FA password if present.<br/>
  18. * the password hint will be sent in the hint param */
  19. password?: (hint?: string) => Promise<string>;
  20. /** in case of a new account creation this callback should return a first name and last name `[first,last]`. */
  21. firstAndLastNames?: () => Promise<[string, string?]>;
  22. /** a qrCode token for login through qrCode.<br/>
  23. * this would need a QR code that you should scan with another app to login with. */
  24. qrCode?: (qrCode: { token: Buffer; expires: number }) => Promise<void>;
  25. /** when an error happens during auth this function will be called with the error.<br/>
  26. * if this returns true the auth operation will stop. */
  27. onError: (err: Error) => Promise<boolean> | void;
  28. /** whether to send the code through SMS or not. */
  29. forceSMS?: boolean;
  30. }
  31. export interface UserPasswordAuthParams {
  32. /** optional string or callback that should return the 2FA password if present.<br/>
  33. * the password hint will be sent in the hint param */
  34. password?: (hint?: string) => Promise<string>;
  35. /** when an error happens during auth this function will be called with the error.<br/>
  36. * if this returns true the auth operation will stop. */
  37. onError: (err: Error) => Promise<boolean> | void;
  38. }
  39. export interface QrCodeAuthParams extends UserPasswordAuthParams {
  40. /** a qrCode token for login through qrCode.<br/>
  41. * this would need a QR code that you should scan with another app to login with. */
  42. qrCode?: (qrCode: { token: Buffer; expires: number }) => Promise<void>;
  43. /** when an error happens during auth this function will be called with the error.<br/>
  44. * if this returns true the auth operation will stop. */
  45. onError: (err: Error) => Promise<boolean> | void;
  46. }
  47. interface ReturnString {
  48. (): string;
  49. }
  50. /**
  51. * For when you want as a normal bot created by https://t.me/Botfather.<br/>
  52. * Logging in as bot is simple and requires no callbacks
  53. */
  54. export interface BotAuthParams {
  55. /**
  56. * the bot token to use.
  57. */
  58. botAuthToken: string | ReturnString;
  59. }
  60. /**
  61. * Credential needed for the authentication. you can get theses from https://my.telegram.org/auth<br/>
  62. * Note: This is required for both logging in as a bot and a user.<br/>
  63. */
  64. export interface ApiCredentials {
  65. /** The app api id. */
  66. apiId: number;
  67. /** the app api hash */
  68. apiHash: string;
  69. }
  70. const QR_CODE_TIMEOUT = 30000;
  71. // region public methods
  72. /** @hidden */
  73. export async function start(
  74. client: TelegramClient,
  75. authParams: UserAuthParams | BotAuthParams
  76. ) {
  77. if (!client.connected) {
  78. await client.connect();
  79. }
  80. if (await client.checkAuthorization()) {
  81. return;
  82. }
  83. const apiCredentials = {
  84. apiId: client.apiId,
  85. apiHash: client.apiHash,
  86. };
  87. await _authFlow(client, apiCredentials, authParams);
  88. }
  89. /** @hidden */
  90. export async function checkAuthorization(client: TelegramClient) {
  91. try {
  92. await client.invoke(new Api.updates.GetState());
  93. return true;
  94. } catch (e) {
  95. return false;
  96. }
  97. }
  98. /** @hidden */
  99. export async function signInUser(
  100. client: TelegramClient,
  101. apiCredentials: ApiCredentials,
  102. authParams: UserAuthParams
  103. ): Promise<Api.TypeUser> {
  104. let phoneNumber;
  105. let phoneCodeHash;
  106. let isCodeViaApp = false;
  107. while (1) {
  108. try {
  109. if (typeof authParams.phoneNumber === "function") {
  110. try {
  111. phoneNumber = await authParams.phoneNumber();
  112. } catch (err: any) {
  113. if (err.errorMessage === "RESTART_AUTH_WITH_QR") {
  114. return client.signInUserWithQrCode(
  115. apiCredentials,
  116. authParams
  117. );
  118. }
  119. throw err;
  120. }
  121. } else {
  122. phoneNumber = authParams.phoneNumber;
  123. }
  124. const sendCodeResult = await client.sendCode(
  125. apiCredentials,
  126. phoneNumber,
  127. authParams.forceSMS
  128. );
  129. phoneCodeHash = sendCodeResult.phoneCodeHash;
  130. isCodeViaApp = sendCodeResult.isCodeViaApp;
  131. if (typeof phoneCodeHash !== "string") {
  132. throw new Error("Failed to retrieve phone code hash");
  133. }
  134. break;
  135. } catch (err: any) {
  136. if (typeof authParams.phoneNumber !== "function") {
  137. throw err;
  138. }
  139. const shouldWeStop = await authParams.onError(err);
  140. if (shouldWeStop) {
  141. throw new Error("AUTH_USER_CANCEL");
  142. }
  143. }
  144. }
  145. let phoneCode;
  146. let isRegistrationRequired = false;
  147. let termsOfService;
  148. while (1) {
  149. try {
  150. try {
  151. phoneCode = await authParams.phoneCode(isCodeViaApp);
  152. } catch (err: any) {
  153. // This is the support for changing phone number from the phone code screen.
  154. if (err.errorMessage === "RESTART_AUTH") {
  155. return client.signInUser(apiCredentials, authParams);
  156. }
  157. }
  158. if (!phoneCode) {
  159. throw new Error("Code is empty");
  160. }
  161. // May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
  162. // PhoneCodeHashEmptyError or PhoneCodeInvalidError.
  163. const result = await client.invoke(
  164. new Api.auth.SignIn({
  165. phoneNumber,
  166. phoneCodeHash,
  167. phoneCode,
  168. })
  169. );
  170. if (result instanceof Api.auth.AuthorizationSignUpRequired) {
  171. isRegistrationRequired = true;
  172. termsOfService = result.termsOfService;
  173. break;
  174. }
  175. return result.user;
  176. } catch (err: any) {
  177. if (err.errorMessage === "SESSION_PASSWORD_NEEDED") {
  178. return client.signInWithPassword(apiCredentials, authParams);
  179. } else {
  180. const shouldWeStop = await authParams.onError(err);
  181. if (shouldWeStop) {
  182. throw new Error("AUTH_USER_CANCEL");
  183. }
  184. }
  185. }
  186. }
  187. if (isRegistrationRequired) {
  188. while (1) {
  189. try {
  190. let lastName;
  191. let firstName = "first name";
  192. if (authParams.firstAndLastNames) {
  193. const result = await authParams.firstAndLastNames();
  194. firstName = result[0];
  195. lastName = result[1];
  196. }
  197. if (!firstName) {
  198. throw new Error("First name is required");
  199. }
  200. const { user } = (await client.invoke(
  201. new Api.auth.SignUp({
  202. phoneNumber,
  203. phoneCodeHash,
  204. firstName,
  205. lastName,
  206. })
  207. )) as Api.auth.Authorization;
  208. if (termsOfService) {
  209. // This is a violation of Telegram rules: the user should be presented with and accept TOS.
  210. await client.invoke(
  211. new Api.help.AcceptTermsOfService({
  212. id: termsOfService.id,
  213. })
  214. );
  215. }
  216. return user;
  217. } catch (err: any) {
  218. const shouldWeStop = await authParams.onError(err);
  219. if (shouldWeStop) {
  220. throw new Error("AUTH_USER_CANCEL");
  221. }
  222. }
  223. }
  224. }
  225. await authParams.onError(new Error("Auth failed"));
  226. return client.signInUser(apiCredentials, authParams);
  227. }
  228. /** @hidden */
  229. export async function signInUserWithQrCode(
  230. client: TelegramClient,
  231. apiCredentials: ApiCredentials,
  232. authParams: QrCodeAuthParams
  233. ): Promise<Api.TypeUser> {
  234. const inputPromise = (async () => {
  235. while (1) {
  236. const result = await client.invoke(
  237. new Api.auth.ExportLoginToken({
  238. apiId: Number(apiCredentials.apiId),
  239. apiHash: apiCredentials.apiHash,
  240. exceptIds: [],
  241. })
  242. );
  243. if (!(result instanceof Api.auth.LoginToken)) {
  244. throw new Error("Unexpected");
  245. }
  246. const { token, expires } = result;
  247. if (authParams.qrCode) {
  248. await Promise.race([
  249. authParams.qrCode({ token, expires }),
  250. sleep(QR_CODE_TIMEOUT),
  251. ]);
  252. }
  253. await sleep(QR_CODE_TIMEOUT);
  254. }
  255. })();
  256. const updatePromise = new Promise((resolve) => {
  257. client.addEventHandler((update: Api.TypeUpdate) => {
  258. if (update instanceof Api.UpdateLoginToken) {
  259. resolve(undefined);
  260. }
  261. });
  262. });
  263. try {
  264. await Promise.race([updatePromise, inputPromise]);
  265. } catch (err) {
  266. throw err;
  267. }
  268. try {
  269. const result2 = await client.invoke(
  270. new Api.auth.ExportLoginToken({
  271. apiId: Number(apiCredentials.apiId),
  272. apiHash: apiCredentials.apiHash,
  273. exceptIds: [],
  274. })
  275. );
  276. if (
  277. result2 instanceof Api.auth.LoginTokenSuccess &&
  278. result2.authorization instanceof Api.auth.Authorization
  279. ) {
  280. return result2.authorization.user;
  281. } else if (result2 instanceof Api.auth.LoginTokenMigrateTo) {
  282. await client._switchDC(result2.dcId);
  283. const migratedResult = await client.invoke(
  284. new Api.auth.ImportLoginToken({
  285. token: result2.token,
  286. })
  287. );
  288. if (
  289. migratedResult instanceof Api.auth.LoginTokenSuccess &&
  290. migratedResult.authorization instanceof Api.auth.Authorization
  291. ) {
  292. return migratedResult.authorization.user;
  293. } else {
  294. client._log.error(
  295. `Received unknown result while scanning QR ${result2.className}`
  296. );
  297. throw new Error(
  298. `Received unknown result while scanning QR ${result2.className}`
  299. );
  300. }
  301. } else {
  302. client._log.error(
  303. `Received unknown result while scanning QR ${result2.className}`
  304. );
  305. throw new Error(
  306. `Received unknown result while scanning QR ${result2.className}`
  307. );
  308. }
  309. } catch (err: any) {
  310. if (err.errorMessage === "SESSION_PASSWORD_NEEDED") {
  311. return client.signInWithPassword(apiCredentials, authParams);
  312. }
  313. throw err;
  314. }
  315. await authParams.onError(new Error("QR auth failed"));
  316. throw new Error("QR auth failed");
  317. }
  318. /** @hidden */
  319. export async function sendCode(
  320. client: TelegramClient,
  321. apiCredentials: ApiCredentials,
  322. phoneNumber: string,
  323. forceSMS = false
  324. ): Promise<{
  325. phoneCodeHash: string;
  326. isCodeViaApp: boolean;
  327. }> {
  328. try {
  329. const { apiId, apiHash } = apiCredentials;
  330. const sendResult = await client.invoke(
  331. new Api.auth.SendCode({
  332. phoneNumber,
  333. apiId,
  334. apiHash,
  335. settings: new Api.CodeSettings({}),
  336. })
  337. );
  338. // If we already sent a SMS, do not resend the phoneCode (hash may be empty)
  339. if (!forceSMS || sendResult.type instanceof Api.auth.SentCodeTypeSms) {
  340. return {
  341. phoneCodeHash: sendResult.phoneCodeHash,
  342. isCodeViaApp:
  343. sendResult.type instanceof Api.auth.SentCodeTypeApp,
  344. };
  345. }
  346. const resendResult = await client.invoke(
  347. new Api.auth.ResendCode({
  348. phoneNumber,
  349. phoneCodeHash: sendResult.phoneCodeHash,
  350. })
  351. );
  352. return {
  353. phoneCodeHash: resendResult.phoneCodeHash,
  354. isCodeViaApp: resendResult.type instanceof Api.auth.SentCodeTypeApp,
  355. };
  356. } catch (err: any) {
  357. if (err.errorMessage === "AUTH_RESTART") {
  358. return client.sendCode(apiCredentials, phoneNumber, forceSMS);
  359. } else {
  360. throw err;
  361. }
  362. }
  363. }
  364. /** @hidden */
  365. export async function signInWithPassword(
  366. client: TelegramClient,
  367. apiCredentials: ApiCredentials,
  368. authParams: UserPasswordAuthParams
  369. ): Promise<Api.TypeUser> {
  370. let emptyPassword = false;
  371. while (1) {
  372. try {
  373. const passwordSrpResult = await client.invoke(
  374. new Api.account.GetPassword()
  375. );
  376. if (!authParams.password) {
  377. emptyPassword = true;
  378. break;
  379. }
  380. const password = await authParams.password(passwordSrpResult.hint);
  381. if (!password) {
  382. throw new Error("Password is empty");
  383. }
  384. const passwordSrpCheck = await computePasswordSrpCheck(
  385. passwordSrpResult,
  386. password
  387. );
  388. const { user } = (await client.invoke(
  389. new Api.auth.CheckPassword({
  390. password: passwordSrpCheck,
  391. })
  392. )) as Api.auth.Authorization;
  393. return user;
  394. } catch (err: any) {
  395. const shouldWeStop = await authParams.onError(err);
  396. if (shouldWeStop) {
  397. throw new Error("AUTH_USER_CANCEL");
  398. }
  399. }
  400. }
  401. if (emptyPassword) {
  402. throw new Error("Account has 2FA enabled.");
  403. }
  404. return undefined!; // Never reached (TypeScript fix)
  405. }
  406. /** @hidden */
  407. export async function signInBot(
  408. client: TelegramClient,
  409. apiCredentials: ApiCredentials,
  410. authParams: BotAuthParams
  411. ) {
  412. const { apiId, apiHash } = apiCredentials;
  413. let { botAuthToken } = authParams;
  414. if (!botAuthToken) {
  415. throw new Error("a valid BotToken is required");
  416. }
  417. if (typeof botAuthToken === "function") {
  418. let token;
  419. while (true) {
  420. token = await botAuthToken();
  421. if (token) {
  422. botAuthToken = token;
  423. break;
  424. }
  425. }
  426. }
  427. const { user } = (await client.invoke(
  428. new Api.auth.ImportBotAuthorization({
  429. apiId,
  430. apiHash,
  431. botAuthToken,
  432. })
  433. )) as Api.auth.Authorization;
  434. return user;
  435. }
  436. /** @hidden */
  437. export async function _authFlow(
  438. client: TelegramClient,
  439. apiCredentials: ApiCredentials,
  440. authParams: UserAuthParams | BotAuthParams
  441. ) {
  442. const me =
  443. "phoneNumber" in authParams
  444. ? await client.signInUser(apiCredentials, authParams)
  445. : await client.signInBot(apiCredentials, authParams);
  446. client._log.info("Signed in successfully as " + utils.getDisplayName(me));
  447. }