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
| Name | Type | Required | Description |
|---|---|---|---|
code | string | yes | 6-digit TOTP code from authenticator app |
flowId | string | no | Flow 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
| Name | Type | Required | Description |
|---|---|---|---|
displayName | string | yes | Human-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
| Name | Type | Required | Description |
|---|---|---|---|
credentialId | string | yes | ID 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
| Name | Type | Required | Description |
|---|---|---|---|
code | string | yes | One 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 class | HTTP status | When |
|---|---|---|
InvalidCodeError | 400 | Wrong TOTP/recovery code |
ExpiredFlowError | 410 | Enrollment flow expired |
MfaAlreadyEnrolledError | 409 | TOTP already enrolled |
MfaRequiredError | 403 | Cannot remove last MFA method when required |
NotAllowedError | N/A | User cancelled WebAuthn dialog (browser) |
RateLimitError | 429 | Too many verification attempts |