Skip to content

Two-factor without the screens (SMS, WhatsApp) ​

What it is for. Finishing a two-factor login for an account that confirms with an SMS or a WhatsApp code. The two-factor screens act only on the authenticator app and on an approval on another device (Two-factor login); for an SMS or a WhatsApp code, you compose the login over the app layer with the effects of this page.

What you call. AndroidLogin (the login over the app layer) fails with LoginTwoFactorRequiredError when the account asks for a second step. The effects of this page finish it: look at the methods the account offers, take one, submit its code. The simpler way for an account you control is to turn on the authenticator app (Turn on the authenticator app) and use the screens.

What you get. The logged-in profile, or a typed error that names what went wrong (the table below).

An example ​

The program below logs in over the app layer (Compose the app layer yourself) and, when the account asks for a second step, answers it by the method the account offers. The code is read after it exists (Codes that arrive during the run).

ts
import {
  AndroidApp,
  AndroidLogin,
  AndroidTwoFactorGetMethods,
  AndroidTwoFactorSelectTotp,
  AndroidTwoFactorVerifyCode,
  type LoginTwoFactorRequiredError,
  totpCodeOf,
  TwoFactorBloksSendWhatsAppCode,
} from '@igpapi/android';
import { Data, Effect } from 'effect';
import { createInterface } from 'node:readline/promises';

const username = process.env['IG_USERNAME'] ?? '';
const password = process.env['IG_PASSWORD'] ?? '';
const totpSecret = process.env['IG_TOTP_SECRET'] ?? '';

/** The code did not arrive: nobody typed it, or your system did not answer. */
class CodeUnavailableError extends Data.TaggedError('CodeUnavailableError')<{
  readonly kind: 'sms' | 'whatsapp';
}> {}

/** Waits for a code at the moment it exists ("Codes that arrive during the run"). */
const readCode = (kind: 'sms' | 'whatsapp') =>
  Effect.tryPromise({
    try: async () => {
      const terminal = createInterface({ input: process.stdin, output: process.stdout });
      try {
        return (await terminal.question(`The ${kind} code: `)).trim();
      } finally {
        terminal.close();
      }
    },
    catch: () => new CodeUnavailableError({ kind }),
  }).pipe(Effect.timeout('5 minutes'));

// Finishes the login the account's second step asks for: look at what the account offers, take one, submit its code.
const finishTwoFactor = (error: LoginTwoFactorRequiredError) =>
  Effect.gen(function* () {
    const twoStepVerificationContext = error.challengeContext;
    const picker = yield* AndroidTwoFactorGetMethods({
      twoStepVerificationContext,
      screenId: error.methodPickerScreenId,
    });

    if (picker.methods.includes('whatsapp')) {
      // This sends the code by WhatsApp; the code is read after it exists.
      const { maskedContactPoint } = yield* TwoFactorBloksSendWhatsAppCode({ twoStepVerificationContext });
      const code = yield* readCode('whatsapp');
      return yield* AndroidTwoFactorVerifyCode({
        twoStepVerificationContext,
        code,
        challenge: 'whatsapp',
        maskedContactPoint: maskedContactPoint ?? undefined,
      });
    }
    if (picker.methods.includes('sms')) {
      // The server sent the SMS; the code is read after it exists.
      const code = yield* readCode('sms');
      return yield* AndroidTwoFactorVerifyCode({ twoStepVerificationContext, code, challenge: 'sms' });
    }
    // The authenticator app: the code is computed where it is sent.
    const screen = yield* AndroidTwoFactorSelectTotp({ twoStepVerificationContext, selectQpl: picker.selectQpl });
    return yield* AndroidTwoFactorVerifyCode({
      twoStepVerificationContext,
      code: totpCodeOf(totpSecret),
      challenge: 'totp',
      qplMarkerId: screen.verifyQpl?.markerId,
      qplInstanceId: screen.verifyQpl?.instanceId,
    });
  });

// A login that needs no second step finishes by itself; otherwise the second step runs.
const program = AndroidLogin({ username, password }).pipe(
  Effect.catchTag('LoginTwoFactorRequiredError', finishTwoFactor),
);

await Effect.runPromise(Effect.provide(program, AndroidApp({ seed: username })));

The error and what it carries ​

LoginTwoFactorRequiredError carries:

  • challengeContext: a token every following call passes back as twoStepVerificationContext;
  • resolution: what the server chose first, either approve_from_another_device (the login waits for an approval on another device) or choose_method (the server's default is another method, and methods lists the methods on offer);
  • methodPickerScreenId: the id of the "Try another way" picker, which AndroidTwoFactorGetMethods takes as screenId.

An empty or unreadable answer to the second-step request is a separate error, LoginTwoFactorEntrypointParseError, not LoginTwoFactorRequiredError: a program that handles one handles the other too.

The effects ​

EffectWhat it does
AndroidTwoFactorGetMethodsReads the methods the account offers (approve_from_another_device, whatsapp, totp, sms, backup_codes).
AndroidTwoFactorSelectTotpChooses the authenticator app and opens its code screen.
TwoFactorBloksSendWhatsAppCodeMakes Instagram send the WhatsApp code and answers the masked phone number it went to.
AndroidTwoFactorVerifyCodeSubmits the code (challenge: 'totp', 'whatsapp' or 'sms') and answers the logged-in profile.
AndroidTwoFactorPollTrustedNotificationAsks once whether the approval on another device has come: pending, approved (with the profile) or denied.

The Simulator never polls by itself: for an approval, call AndroidTwoFactorPollTrustedNotification about every 7.5 seconds, as the app does, and bound the wait.

Errors ​

TagWhat it meansWhat to do
LoginTwoFactorRequiredErrorThe account asks for a second step.Finish it with the effects above.
LoginTwoFactorEntrypointParseErrorThe second-step answer was empty or unreadable.Retry; report it if it repeats.
TwoFactorBloksInvalidCodeErrorThe code was wrong, expired, or refused for too many attempts. The reasons are not told apart.Ask for the code again.
TwoFactorBloksLoginResponseNotFoundErrorThe answer to the code carried no login and no rejection.Retry; report it if it repeats.
TwoFactorBloksIssuedIdMissingErrorA value the previous answer must issue was missing.Start the login again.

Every error has a row in Errors.

The pages of this site are the README files of the repository. The API reference is generated from the source.