auth.ts 15 KB

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