394 lines
13 KiB
TypeScript
394 lines
13 KiB
TypeScript
import Api from '../tl/api';
|
|
import type TelegramClient from './TelegramClient';
|
|
import utils from '../Utils';
|
|
import { sleep } from '../Helpers';
|
|
import { computeCheck as computePasswordSrpCheck } from '../Password';
|
|
|
|
export interface UserAuthParams {
|
|
phoneNumber: string | (() => Promise<string>);
|
|
webAuthTokenFailed: () => void;
|
|
phoneCode: (isCodeViaApp?: boolean) => Promise<string>;
|
|
password: (hint?: string, noReset?: boolean) => Promise<string>;
|
|
firstAndLastNames: () => Promise<[string, string?]>;
|
|
qrCode: (qrCode: { token: Buffer; expires: number }) => Promise<void>;
|
|
onError: (err: Error) => void;
|
|
forceSMS?: boolean;
|
|
initialMethod?: 'phoneNumber' | 'qrCode';
|
|
shouldThrowIfUnauthorized?: boolean;
|
|
webAuthToken?: string;
|
|
mockScenario?: string;
|
|
}
|
|
|
|
export interface BotAuthParams {
|
|
botAuthToken: string;
|
|
}
|
|
|
|
interface ApiCredentials {
|
|
apiId: number;
|
|
apiHash: string;
|
|
}
|
|
|
|
const DEFAULT_INITIAL_METHOD = 'phoneNumber';
|
|
const QR_CODE_TIMEOUT = 30000;
|
|
|
|
export async function authFlow(
|
|
client: TelegramClient,
|
|
apiCredentials: ApiCredentials,
|
|
authParams: UserAuthParams | BotAuthParams,
|
|
) {
|
|
let me: Api.TypeUser;
|
|
|
|
if ('botAuthToken' in authParams) {
|
|
me = await signInBot(client, apiCredentials, authParams);
|
|
} else if ('webAuthToken' in authParams && authParams.webAuthToken) {
|
|
me = await signInUserWithWebToken(client, apiCredentials, authParams);
|
|
} else {
|
|
me = await signInUserWithPreferredMethod(client, apiCredentials, authParams);
|
|
}
|
|
|
|
client._log.info('Signed in successfully as', utils.getDisplayName(me));
|
|
}
|
|
|
|
export function signInUserWithPreferredMethod(
|
|
client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams,
|
|
): Promise<Api.TypeUser> {
|
|
const { initialMethod = DEFAULT_INITIAL_METHOD } = authParams;
|
|
|
|
if (initialMethod === 'phoneNumber') {
|
|
return signInUser(client, apiCredentials, authParams);
|
|
} else {
|
|
return signInUserWithQrCode(client, apiCredentials, authParams);
|
|
}
|
|
}
|
|
|
|
export async function checkAuthorization(client: TelegramClient, shouldThrow = false) {
|
|
try {
|
|
await client.invoke(new Api.updates.GetState());
|
|
return true;
|
|
} catch (e: any) {
|
|
if (e.message === 'Disconnect' || shouldThrow) throw e;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function signInUserWithWebToken(
|
|
client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams,
|
|
): Promise<Api.TypeUser> {
|
|
try {
|
|
const { apiId, apiHash } = apiCredentials;
|
|
const sendResult = await client.invoke(new Api.auth.ImportWebTokenAuthorization({
|
|
webAuthToken: authParams.webAuthToken,
|
|
apiId,
|
|
apiHash,
|
|
}));
|
|
|
|
if (sendResult instanceof Api.auth.Authorization) {
|
|
return sendResult.user;
|
|
} else {
|
|
throw new Error('SIGN_UP_REQUIRED');
|
|
}
|
|
} catch (err: any) {
|
|
if (err.message === 'SESSION_PASSWORD_NEEDED') {
|
|
return signInWithPassword(client, apiCredentials, authParams, true);
|
|
} else {
|
|
client._log.error(`Failed to login with web token: ${err}`);
|
|
authParams.webAuthTokenFailed();
|
|
return signInUserWithPreferredMethod(client, apiCredentials, {
|
|
...authParams,
|
|
webAuthToken: undefined,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async function signInUser(
|
|
client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams,
|
|
): Promise<Api.TypeUser> {
|
|
let phoneNumber;
|
|
let phoneCodeHash;
|
|
let isCodeViaApp = false;
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
try {
|
|
if (typeof authParams.phoneNumber === 'function') {
|
|
try {
|
|
phoneNumber = await authParams.phoneNumber();
|
|
} catch (err: any) {
|
|
if (err.message === 'RESTART_AUTH_WITH_QR') {
|
|
return signInUserWithQrCode(client, apiCredentials, authParams);
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
} else {
|
|
phoneNumber = authParams.phoneNumber;
|
|
}
|
|
const sendCodeResult = await sendCode(client, apiCredentials, phoneNumber, authParams.forceSMS);
|
|
phoneCodeHash = sendCodeResult.phoneCodeHash;
|
|
isCodeViaApp = sendCodeResult.isCodeViaApp;
|
|
|
|
if (typeof phoneCodeHash !== 'string') {
|
|
throw new Error('Failed to retrieve phone code hash');
|
|
}
|
|
|
|
break;
|
|
} catch (err: any) {
|
|
if (typeof authParams.phoneNumber !== 'function') {
|
|
throw err;
|
|
}
|
|
|
|
authParams.onError(err);
|
|
}
|
|
}
|
|
|
|
let phoneCode;
|
|
let isRegistrationRequired = false;
|
|
let termsOfService;
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (1) {
|
|
try {
|
|
try {
|
|
phoneCode = await authParams.phoneCode(isCodeViaApp);
|
|
} catch (err: any) {
|
|
// This is the support for changing phone number from the phone code screen.
|
|
if (err.message === 'RESTART_AUTH') {
|
|
return signInUser(client, apiCredentials, authParams);
|
|
}
|
|
}
|
|
|
|
if (!phoneCode) {
|
|
throw new Error('Code is empty');
|
|
}
|
|
|
|
// May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
|
// PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
|
const result = await client.invoke(new Api.auth.SignIn({
|
|
phoneNumber,
|
|
phoneCodeHash,
|
|
phoneCode,
|
|
}));
|
|
|
|
if (result instanceof Api.auth.AuthorizationSignUpRequired) {
|
|
isRegistrationRequired = true;
|
|
termsOfService = result.termsOfService;
|
|
break;
|
|
}
|
|
|
|
return result.user;
|
|
} catch (err: any) {
|
|
if (err.message === 'SESSION_PASSWORD_NEEDED') {
|
|
return signInWithPassword(client, apiCredentials, authParams);
|
|
} else {
|
|
authParams.onError(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isRegistrationRequired) {
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (1) {
|
|
try {
|
|
const [firstName, lastName] = await authParams.firstAndLastNames();
|
|
if (!firstName) {
|
|
throw new Error('First name is required');
|
|
}
|
|
|
|
const { user } = await client.invoke(new Api.auth.SignUp({
|
|
phoneNumber,
|
|
phoneCodeHash,
|
|
firstName,
|
|
lastName,
|
|
})) as Api.auth.Authorization;
|
|
|
|
if (termsOfService) {
|
|
// This is a violation of Telegram rules: the user should be presented with and accept TOS.
|
|
await client.invoke(new Api.help.AcceptTermsOfService({ id: termsOfService.id }));
|
|
}
|
|
|
|
return user;
|
|
} catch (err: any) {
|
|
authParams.onError(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
authParams.onError(new Error('Auth failed'));
|
|
return signInUser(client, apiCredentials, authParams);
|
|
}
|
|
|
|
async function signInUserWithQrCode(
|
|
client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams,
|
|
): Promise<Api.TypeUser> {
|
|
let isScanningComplete = false;
|
|
|
|
const inputPromise = (async () => {
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (1) {
|
|
if (isScanningComplete) {
|
|
break;
|
|
}
|
|
|
|
const result = await client.invoke(new Api.auth.ExportLoginToken({
|
|
apiId: Number(process.env.TELEGRAM_API_ID),
|
|
apiHash: process.env.TELEGRAM_API_HASH,
|
|
exceptIds: [],
|
|
}));
|
|
if (!(result instanceof Api.auth.LoginToken)) {
|
|
throw new Error('Unexpected');
|
|
}
|
|
|
|
const { token, expires } = result;
|
|
|
|
await Promise.race([
|
|
authParams.qrCode({ token, expires }),
|
|
sleep(QR_CODE_TIMEOUT),
|
|
]);
|
|
}
|
|
})();
|
|
|
|
const updatePromise = new Promise<void>((resolve) => {
|
|
client.addEventHandler((update: Api.TypeUpdate) => {
|
|
if (update instanceof Api.UpdateLoginToken) {
|
|
resolve();
|
|
}
|
|
}, { build: (update: object) => update });
|
|
});
|
|
|
|
try {
|
|
// Either we receive an update that QR is successfully scanned,
|
|
// or we receive a rejection caused by user going back to the regular auth form
|
|
await Promise.race([updatePromise, inputPromise]);
|
|
} catch (err: any) {
|
|
if (err.message === 'RESTART_AUTH') {
|
|
return await signInUser(client, apiCredentials, authParams);
|
|
}
|
|
|
|
throw err;
|
|
} finally {
|
|
isScanningComplete = true;
|
|
}
|
|
|
|
try {
|
|
const result2 = await client.invoke(new Api.auth.ExportLoginToken({
|
|
apiId: Number(process.env.TELEGRAM_API_ID),
|
|
apiHash: process.env.TELEGRAM_API_HASH,
|
|
exceptIds: [],
|
|
}));
|
|
|
|
if (result2 instanceof Api.auth.LoginTokenSuccess && result2.authorization instanceof Api.auth.Authorization) {
|
|
return result2.authorization.user;
|
|
} else if (result2 instanceof Api.auth.LoginTokenMigrateTo) {
|
|
await client._switchDC(result2.dcId);
|
|
const migratedResult = await client.invoke(new Api.auth.ImportLoginToken({
|
|
token: result2.token,
|
|
}));
|
|
|
|
if (migratedResult instanceof Api.auth.LoginTokenSuccess
|
|
&& migratedResult.authorization instanceof Api.auth.Authorization) {
|
|
return migratedResult.authorization.user;
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
if (err.message === 'SESSION_PASSWORD_NEEDED') {
|
|
return signInWithPassword(client, apiCredentials, authParams);
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
// This is a workaround for TypeScript (never actually reached)
|
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
|
throw undefined;
|
|
}
|
|
|
|
async function sendCode(
|
|
client: TelegramClient, apiCredentials: ApiCredentials, phoneNumber: string, forceSMS = false,
|
|
): Promise<{
|
|
phoneCodeHash: string;
|
|
isCodeViaApp: boolean;
|
|
}> {
|
|
try {
|
|
const { apiId, apiHash } = apiCredentials;
|
|
const sendResult = await client.invoke(new Api.auth.SendCode({
|
|
phoneNumber,
|
|
apiId,
|
|
apiHash,
|
|
settings: new Api.CodeSettings(),
|
|
}));
|
|
|
|
if (!(sendResult instanceof Api.auth.SentCode)) {
|
|
throw Error('Unexpected SentCodeSuccess');
|
|
}
|
|
|
|
// If we already sent a SMS, do not resend the phoneCode (hash may be empty)
|
|
if (!forceSMS || (sendResult.type instanceof Api.auth.SentCodeTypeSms)) {
|
|
return {
|
|
phoneCodeHash: sendResult.phoneCodeHash,
|
|
isCodeViaApp: sendResult.type instanceof Api.auth.SentCodeTypeApp,
|
|
};
|
|
}
|
|
|
|
const resendResult = await client.invoke(new Api.auth.ResendCode({
|
|
phoneNumber,
|
|
phoneCodeHash: sendResult.phoneCodeHash,
|
|
}));
|
|
|
|
if (!(resendResult instanceof Api.auth.SentCode)) {
|
|
throw Error('Unexpected SentCodeSuccess');
|
|
}
|
|
|
|
return {
|
|
phoneCodeHash: resendResult.phoneCodeHash,
|
|
isCodeViaApp: resendResult.type instanceof Api.auth.SentCodeTypeApp,
|
|
};
|
|
} catch (err: any) {
|
|
if (err.message === 'AUTH_RESTART') {
|
|
return sendCode(client, apiCredentials, phoneNumber, forceSMS);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function signInWithPassword(
|
|
client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams, noReset = false,
|
|
): Promise<Api.TypeUser> {
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (1) {
|
|
try {
|
|
const passwordSrpResult = await client.invoke(new Api.account.GetPassword());
|
|
const password = await authParams.password(passwordSrpResult.hint, noReset);
|
|
if (!password) {
|
|
throw new Error('Password is empty');
|
|
}
|
|
|
|
const passwordSrpCheck = await computePasswordSrpCheck(passwordSrpResult, password);
|
|
const { user } = await client.invoke(new Api.auth.CheckPassword({
|
|
password: passwordSrpCheck,
|
|
})) as Api.auth.Authorization;
|
|
|
|
return user;
|
|
} catch (err: any) {
|
|
authParams.onError(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unreachable
|
|
return undefined!; // Never reached (TypeScript fix)
|
|
}
|
|
|
|
async function signInBot(client: TelegramClient, apiCredentials: ApiCredentials, authParams: BotAuthParams) {
|
|
const { apiId, apiHash } = apiCredentials;
|
|
const { botAuthToken } = authParams;
|
|
|
|
const { user } = await client.invoke(new Api.auth.ImportBotAuthorization({
|
|
apiId,
|
|
apiHash,
|
|
botAuthToken,
|
|
})) as Api.auth.Authorization;
|
|
|
|
return user;
|
|
}
|