From 7917c7fe2b8af3ff93ee6165e2e357dc0730c324 Mon Sep 17 00:00:00 2001 From: Fabian Haas Date: Mon, 25 Mar 2024 18:00:51 +0100 Subject: [PATCH] add 2FA required exception --- src/index.ts | 18 ++++++++++++------ src/instagram.ts | 17 ++++++++++++++++- test/instagram.test.ts | 36 +++++++++++++++++++++++------------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/index.ts b/src/index.ts index a1d3757..beaa8aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,18 @@ import * as prompt from '@inquirer/prompts'; import {encryptPassword, fetchVerification, login} from "./instagram"; +import {ExitPromptError} from "@inquirer/prompts"; async function authenticate() { const verification = await fetchVerification() while (true) { - try { - const user = await prompt.input({message: "Instagram username, phone number, or email: "}) - const password = await prompt.password({message: "Password: "}) + const user = await prompt.input({message: "Instagram username, phone number, or email: "}) + const password = await prompt.password({message: "Password: "}) - const encryptedPassword = await encryptPassword({password, key: verification.key}) + const encryptedPassword = await encryptPassword({password, key: verification.key}) + try { return await login({user, password: encryptedPassword, verification}) } catch (e) { console.error((e as Error).message) @@ -19,5 +20,10 @@ async function authenticate() { } } -const sessionId = await authenticate() -console.dir({sessionId}) +try { + console.dir({sessionId: await authenticate()}) +} catch (e) { + if (!(e instanceof ExitPromptError)) { + console.error(e) + } +} diff --git a/src/instagram.ts b/src/instagram.ts index 6b93caf..57968a7 100644 --- a/src/instagram.ts +++ b/src/instagram.ts @@ -4,6 +4,12 @@ import sealBox from "tweetnacl-sealedbox-js"; const crypto = globalThis.crypto const encoder = new TextEncoder() +export class TwoFactorRequired extends Error { + constructor() { + super("Two factor authentication is enabled for this account."); + } +} + export interface InstagramEncryptionKey { public: string, id: number, @@ -117,7 +123,16 @@ export async function login({user, password, verification}: { if (!response.ok) { if (response.headers.get("Content-Type").startsWith("application/json;")) { - throw new Error((await response.json()).message ?? "Login attempted failed.") + const data = await response.json() as { + message?: string, + two_factor_required?: boolean + } + + if (data.two_factor_required) { + throw new TwoFactorRequired() + } + + throw new Error(data.message ?? "Login attempted failed.") } else { throw new Error(await response.text()) } diff --git a/test/instagram.test.ts b/test/instagram.test.ts index 0a532b6..c954178 100644 --- a/test/instagram.test.ts +++ b/test/instagram.test.ts @@ -4,7 +4,7 @@ import { encryptPassword, fetchVerification, InstagramEncryptionKey, - login, + login, TwoFactorRequired, VerificationData } from "../src/instagram"; @@ -198,7 +198,7 @@ describe("Login request handler", () => { expect(result).toStrictEqual(sessionId) }) - describe("Throws exception on failed login", () => { + describe("Throws on failed login", () => { test.each([undefined, "Received error description"])("Message: %s", async (message) => { const headers = new Headers() headers.set("Content-Type", "application/json; charset=utf-8") @@ -229,18 +229,14 @@ describe("Login request handler", () => { } as Response)) try { - await login({ - user: "user", - password: encryptedPassword, - verification - }) + await login({user: "user", password: encryptedPassword, verification}) } catch (e) { expect.assertions(1) expect(e.message).toStrictEqual(expect.any(String)) } }) - test("Throws exception on failed request", async () => { + test("Throws on failed request", async () => { const message = "Error message" const headers = new Headers() @@ -253,14 +249,28 @@ describe("Login request handler", () => { } as Response)) try { - await login({ - user: "user", - password: encryptedPassword, - verification - }) + await login({user: "user", password: encryptedPassword, verification}) } catch (e) { expect.assertions(1) expect(e.message).toStrictEqual(message) } }) + + test("Throws if 2FA is required", async () => { + const headers = new Headers() + headers.set("Content-Type", "application/json; charset=utf-8") + + jest.spyOn(global, "fetch").mockImplementation(() => Promise.resolve({ + ok: false, + json: () => Promise.resolve({two_factor_required: true}), + headers + } as Response)) + + try { + await login({user: "user", password: encryptedPassword, verification}) + } catch (e) { + expect.assertions(1) + expect(e).toBeInstanceOf(TwoFactorRequired) + } + }) })