Avnology ID
SDKsTypeScript SDKAuthentication

Multi-Factor Authentication

Enroll and verify TOTP, passkeys, and recovery codes with the TypeScript SDK.

Multi-Factor Authentication

The SDK supports enrolling and verifying multiple MFA methods: TOTP (time-based one-time passwords), passkeys/security keys, and recovery codes.

enrollTotp()

Begin TOTP enrollment. Returns a QR code URI and secret key for the user to scan or enter in their authenticator app.

enrollTotp(): Promise<TotpEnrollment>

Returns

interface TotpEnrollment {
  flowId: string;       // Flow ID for verification
  secretKey: string;    // Base32-encoded secret (for manual entry)
  qrCodeUri: string;    // otpauth:// URI for QR code generation
  issuer: string;       // "Avnology ID"
  accountName: string;  // User's email
}

Basic usage

import { AvnologyId } from "@avnology/sdk-typescript";

const client = new AvnologyId({
  baseUrl: "https://api.id.avnology.com",
  clientId: "app_abc123",
});

// Step 1: Start enrollment
const totp = await client.enrollTotp();

// Step 2: Display QR code to user (use any QR library)
displayQrCode(totp.qrCodeUri);
// Or show the manual entry key
showSecretKey(totp.secretKey);

// Step 3: User scans QR code and enters the 6-digit code
const code = await getUserInput("Enter the 6-digit code from your app:");

// Step 4: Verify the code to complete enrollment
await client.verifyTotp({
  flowId: totp.flowId,
  code,
});

console.log("TOTP enrollment complete!");

Full enrollment flow with error handling

import { InvalidCodeError, RateLimitError } from "@avnology/sdk-typescript";

async function enrollTotpFlow() {
  const totp = await client.enrollTotp();
  displayQrCode(totp.qrCodeUri);
  displayManualKey(totp.secretKey);

  let attempts = 0;
  const maxAttempts = 3;

  while (attempts < maxAttempts) {
    const code = await getUserInput("Enter code:");
    try {
      await client.verifyTotp({ flowId: totp.flowId, code });
      showSuccess("TOTP is now enabled on your account.");

      // Show recovery codes after MFA enrollment
      const codes = await client.generateRecoveryCodes();
      showRecoveryCodes(codes.codes);
      return;
    } catch (error) {
      if (error instanceof InvalidCodeError) {
        attempts++;
        showError(`Invalid code. ${maxAttempts - attempts} attempts remaining.`);
      } else {
        throw error;
      }
    }
  }

  showError("Too many failed attempts. Please try again.");
}

verifyTotp()

Verify a TOTP code. Used during enrollment (to confirm setup) and during login (as second factor).

verifyTotp(params: VerifyTotpParams): Promise<Session>

Parameters

NameTypeRequiredDescription
codestringyes6-digit TOTP code from authenticator app
flowIdstringnoFlow ID (required during enrollment, auto-detected during login)

Returns

Promise<Session> -- Updated session with AAL2 assurance level.

During login MFA challenge

// After login() throws MfaRequiredError
import { MfaRequiredError, InvalidCodeError } from "@avnology/sdk-typescript";

try {
  await client.login({ email, password });
} catch (error) {
  if (error instanceof MfaRequiredError) {
    // Show TOTP input form
    const code = await getUserInput("Enter your authenticator code:");
    try {
      const session = await client.verifyTotp({ code });
      console.log(session.authenticatorAssuranceLevel); // "aal2"
    } catch (verifyError) {
      if (verifyError instanceof InvalidCodeError) {
        showError("Invalid code. Please try again.");
      }
    }
  }
}

enrollPasskey()

Register a new passkey (FIDO2/WebAuthn credential) for the current user. Triggers the browser's credential creation dialog.

enrollPasskey(params: EnrollPasskeyParams): Promise<PasskeyCredential>

Parameters

NameTypeRequiredDescription
displayNamestringyesHuman-readable name for the passkey (e.g., "My MacBook Pro")

Returns

interface PasskeyCredential {
  id: string;               // Credential ID
  displayName: string;      // Name the user chose
  createdAt: string;        // ISO 8601 timestamp
  lastUsedAt: string | null;
  aaguid: string;           // Authenticator model identifier
  transports: string[];     // ["internal", "hybrid", "usb", "ble", "nfc"]
}

Basic usage

try {
  const credential = await client.enrollPasskey({
    displayName: "My MacBook Pro",
  });

  console.log("Passkey registered:", credential.id);
  console.log("Transports:", credential.transports);
  showSuccess(`Passkey "${credential.displayName}" has been added.`);
} catch (error) {
  if (error.name === "NotAllowedError") {
    showError("Passkey registration was cancelled.");
  } else if (error.name === "InvalidStateError") {
    showError("This passkey is already registered on your account.");
  }
}

List enrolled passkeys

const session = await client.getSession();
const passkeys = session?.identity.credentials?.filter(
  (c) => c.type === "webauthn"
);

for (const passkey of passkeys ?? []) {
  console.log(`${passkey.displayName} (last used: ${passkey.lastUsedAt})`);
}

removePasskey()

Remove a passkey from the user's account.

removePasskey(params: RemovePasskeyParams): Promise<void>

Parameters

NameTypeRequiredDescription
credentialIdstringyesID of the passkey credential to remove

Basic usage

await client.removePasskey({ credentialId: "cred_abc123" });
showSuccess("Passkey removed.");

generateRecoveryCodes()

Generate a new set of recovery codes. This invalidates any previously generated codes.

generateRecoveryCodes(): Promise<RecoveryCodes>

Returns

interface RecoveryCodes {
  codes: string[];  // Array of 12 single-use recovery codes
}

Basic usage

const { codes } = await client.generateRecoveryCodes();

// Display codes to the user -- they are shown only once
console.log("Save these recovery codes in a safe place:");
for (const code of codes) {
  console.log(`  ${code}`);
}
// Example codes: ["abc12-def34", "ghi56-jkl78", ...]

verifyRecoveryCode()

Verify a recovery code during MFA challenge. Each code can only be used once.

verifyRecoveryCode(params: VerifyRecoveryCodeParams): Promise<Session>

Parameters

NameTypeRequiredDescription
codestringyesOne of the 12 recovery codes

Basic usage

import { MfaRequiredError } from "@avnology/sdk-typescript";

try {
  await client.login({ email, password });
} catch (error) {
  if (error instanceof MfaRequiredError) {
    // User can't access their authenticator -- use recovery code
    const code = await getUserInput("Enter a recovery code:");
    const session = await client.verifyRecoveryCode({ code });
    console.log(session.authenticatorAssuranceLevel); // "aal2"

    // Warn user to generate new codes if running low
    showWarning("Consider generating new recovery codes in your account settings.");
  }
}

disableTotp()

Remove TOTP from the user's account. The user must have another MFA method enrolled or MFA must not be required.

disableTotp(): Promise<void>

Basic usage

await client.disableTotp();
showSuccess("TOTP has been removed from your account.");

Common errors

Error classHTTP statusWhen
InvalidCodeError400Wrong TOTP/recovery code
ExpiredFlowError410Enrollment flow expired
MfaAlreadyEnrolledError409TOTP already enrolled
MfaRequiredError403Cannot remove last MFA method when required
NotAllowedErrorN/AUser cancelled WebAuthn dialog (browser)
RateLimitError429Too many verification attempts

See also

  • Login -- MFA during login flow
  • Recovery -- Account recovery
  • Session -- AAL levels and session data

On this page