Skip to content

Commit

Permalink
Allow sessions to be reused (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
hfxbse authored May 1, 2024
1 parent 044f9f6 commit 2b084d3
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 24 deletions.
34 changes: 30 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as prompt from '@inquirer/prompts';
import {
encryptPassword,
fetchVerification,
login,
login, SessionData,
TwoFactorInformation,
TwoFactorRequired,
VerificationData,
Expand All @@ -11,7 +11,7 @@ import {
import {ExitPromptError} from "@inquirer/prompts";


async function authenticate(): Promise<string> {
async function authenticate(): Promise<SessionData> {
const verification = await fetchVerification()

while (true) {
Expand All @@ -36,7 +36,7 @@ async function authenticate(): Promise<string> {
async function twoFactor({verification, info}: {
verification: VerificationData,
info: TwoFactorInformation
}): Promise<string> {
}): Promise<SessionData> {
while (true) {
const code = await prompt.input({message: "Two factor authentication code: "})

Expand All @@ -48,8 +48,34 @@ async function twoFactor({verification, info}: {
}
}

async function readExistingSessionId(): Promise<SessionData> {
while (true) {
const sessionId = await prompt.password({message: "Session id: "})
const userId = parseInt(sessionId.split("%")[0], 10)

if(isNaN(userId)) {
console.log("Session id seems to be invalid. Try again.")
continue
}

return {
id: sessionId,
user: {
id: parseInt(sessionId.split("%")[0], 10)
}
}
}
}


try {
console.dir({sessionId: await authenticate()})
const existingSession = await prompt.confirm({message: "Use an existing session id?", default: false});

const session: SessionData = await (!existingSession ? authenticate() : readExistingSessionId())

if (await prompt.confirm({message: "Show session data?", default: false})) {
console.dir({session})
}
} catch (e) {
if (!(e instanceof ExitPromptError)) {
console.error(e)
Expand Down
51 changes: 41 additions & 10 deletions src/instagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,21 @@ export class TwoFactorRequired extends Error {
}
}

export interface SessionData extends Record<string, any> {
user: {
id: number,
username?: string
},
id: string
}

export interface TwoFactorInformation {
identifier: string,
user: string,
device: string
user: {
username: string,
id: number
},
device: string,
}

export interface InstagramEncryptionKey {
Expand Down Expand Up @@ -127,7 +138,7 @@ export async function login({user, password, verification}: {
user: string,
password: EncryptedPassword,
verification: VerificationData
}): Promise<string> {
}): Promise<SessionData> {
const data = new FormData()
data.set("username", user)
data.set(
Expand All @@ -150,6 +161,7 @@ export async function login({user, password, verification}: {
message?: string,
two_factor_required?: boolean,
two_factor_info?: {
pk: number,
username: string,
two_factor_identifier: string,
device_id: string
Expand All @@ -158,7 +170,10 @@ export async function login({user, password, verification}: {

if (data.two_factor_required) {
throw new TwoFactorRequired({
user: data.two_factor_info.username,
user: {
id: data.two_factor_info.pk,
username: data.two_factor_info.username
},
identifier: data.two_factor_info.two_factor_identifier,
device: data.two_factor_info.device_id
})
Expand All @@ -170,20 +185,30 @@ export async function login({user, password, verification}: {
}
}

if ((await response.json())["authenticated"] !== true) {
throw new Error("Authentication failed.")
const result = (await response.json()) as {
authenticated: boolean,
userId: number
}

return getSessionId(response)
if (result.authenticated !== true) {
throw new Error("Authentication failed. Check your credentials.")
}

return {
id: getSessionId(response),
user: {
id: result.userId
}
}
}

export async function verify2FA({verification, info, code}: {
info: TwoFactorInformation,
verification: VerificationData,
code: string
}): Promise<string> {
}): Promise<SessionData> {
const body = new FormData()
body.set("username", info.user)
body.set("username", info.user.username)
body.set("identifier", info.identifier)
body.set("verificationCode", code)

Expand All @@ -202,5 +227,11 @@ export async function verify2FA({verification, info, code}: {
throw Error(message ?? "Two factor authentication failed.")
}

return getSessionId(response)
return {
id: getSessionId(response),
user: {
id: info.user.id,
username: info.user.username
},
}
}
43 changes: 33 additions & 10 deletions test/instagram.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
encryptPassword,
fetchVerification,
InstagramEncryptionKey,
login, TwoFactorInformation, TwoFactorRequired,
login, SessionData, TwoFactorInformation, TwoFactorRequired,
VerificationData, verify2FA
} from "../src/instagram";

Expand Down Expand Up @@ -182,7 +182,7 @@ function getSessionHeaders() {
return {id, headers}
}

function expectThrowsErrorWithMessage(request: Promise<any>, message: string|undefined = undefined) {
function expectThrowsErrorWithMessage(request: Promise<any>, message: string | undefined = undefined) {
return Promise.all([
expect(request).rejects.toBeInstanceOf(Error),
expect(request).rejects.toStrictEqual(expect.objectContaining({message: message ?? expect.any(String)}))
Expand All @@ -202,16 +202,23 @@ describe("Login request handler", () => {
} as VerificationData
}

test("Returns session id on success", async () => {
test("Returns session data on success", async () => {
const {id, headers} = getSessionHeaders()
const response = {authenticated: true, userId: 1}
const sessionData: SessionData = {
id,
user: {
id: response.userId
}
}

jest.spyOn(global, "fetch").mockImplementation(() => Promise.resolve({
ok: true,
json: () => Promise.resolve({authenticated: true}),
json: () => Promise.resolve(response),
headers
} as Response))

return expect(login(loginData)).resolves.toStrictEqual(id)
return expect(login(loginData)).resolves.toStrictEqual(expect.objectContaining(sessionData))
})

describe("Throws on failed login", () => {
Expand Down Expand Up @@ -254,7 +261,10 @@ describe("Login request handler", () => {
const info: TwoFactorInformation = {
device: "device-id",
identifier: "2fa-id",
user: "user"
user: {
id: 1,
username: "user"
}
}

jest.spyOn(global, "fetch").mockImplementation(() => Promise.resolve({
Expand All @@ -263,7 +273,8 @@ describe("Login request handler", () => {
two_factor_required: true, two_factor_info: {
device_id: info.device,
two_factor_identifier: info.identifier,
username: info.user
username: info.user.username,
pk: info.user.id
}
}),
headers: getJsonHeaders()
Expand All @@ -281,19 +292,31 @@ describe("Login request handler", () => {
describe("Two factor authentication handler", () => {
const requestData = {
verification: {} as VerificationData,
info: {} as TwoFactorInformation,
info: {
user: {
id: 1,
username: "user"
}
} as TwoFactorInformation,
code: "123456"
}

test("Returns session id on success", () => {
test("Returns session data on success", () => {
const {id, headers} = getSessionHeaders()
const sessionData: SessionData = {
id,
user: {
id: requestData.info.user.id,
username: requestData.info.user.username
}
}

jest.spyOn(global, "fetch").mockImplementation(() => Promise.resolve({
ok: true,
headers
} as Response))

return expect(verify2FA(requestData)).resolves.toStrictEqual(id)
return expect(verify2FA(requestData)).resolves.toStrictEqual(expect.objectContaining(sessionData));
});

describe("Throws on failed authentication", () => {
Expand Down

0 comments on commit 2b084d3

Please sign in to comment.