Skip to content
CertiQA is in early access. Request access

2.8 Virtual Auth: 2FA, Email & SMS Verification

How to test a login or signup flow gated behind an authenticator app, an email confirmation code, or an SMS one-time code – without a real phone, inbox, or authenticator app.

Testing Behind an MFA Wall Without a Real Device

Section titled “Testing Behind an MFA Wall Without a Real Device”

A login flow gated behind two-factor auth, or a signup flow that emails or texts a verification code, is otherwise untestable without a real device or a flaky third-party inbox integration – someone has to sit with a phone, read a code, and type it in within a time window, which doesn’t scale to an automated suite that runs on every commit. CertiQA provisions ephemeral virtual inboxes and phone numbers, and generates real TOTP codes from a base32 secret in-process. Email and SMS codes arrive through a provider you choose: a local mock for isolated tests, Mailosaur, or your own HTTP endpoint. No human standing by with a phone is required.

For an authenticator-app-style code, add an INPUT_TOTP step (available in the same action picker as INPUT_TEXT). Set its value to ${TOTP:} – typically the same secret your app’s QR-code enrollment would encode – and CertiQA generates a live, valid 6-digit code at execution time.

For signup and authentication flows that require real inbound email or SMS verification, CertiQA integrates with Mailosaur as the default provider, alongside local ephemeral mock inboxes for isolated testing. Credentials are securely stored in your project Vault: set MAILOSAUR_API_KEY and MAILOSAUR_SERVER_ID (and optionally MAILOSAUR_SMS_NUMBER). When configured, ${EMAIL:<inbox_name>} provisions a real inbound address (@.mailosaur.net) and ${SMS:<phone_label>} resolves to your virtual phone number.

If your organisation already receives test email or SMS somewhere you control (an internal mail catcher, a Twilio webhook, a shared QA mailbox), point CertiQA at a small HTTP endpoint in front of it. CertiQA polls that endpoint for the newest message to the address or number your test used. You need no CertiQA-side code change and no third-party account.

Vault secret (or runner env var) Required Purpose
VIRTUAL_AUTH_HTTP_URL Yes Base URL of your endpoint (http or https). When set, it takes precedence over Mailosaur and the local mock.
VIRTUAL_AUTH_HTTP_TOKEN Recommended Sent as Authorization: Bearer , or as the raw value in the header named below.
VIRTUAL_AUTH_HTTP_AUTH_HEADER No Header name for the token, e.g. X-Api-Key. Default: Authorization.
VIRTUAL_AUTH_HTTP_INBOX_DOMAIN No Turns ${EMAIL:signup} into signup@. Without it, the bare name is used.
VIRTUAL_AUTH_HTTP_SMS_NUMBER No The number ${SMS:label} resolves to, unless the label already starts with +.

CertiQA calls GET <VIRTUAL_AUTH_HTTP_URL>/messages?channel=email|sms&to=

about once a second until the step timeout. Your endpoint answers with:

{
"items": [
{
"id": "msg_123",
"from": "[email protected]",
"subject": "Your verification code",
"body": "Your code is 482913. Or open https://yourapp.com/verify?t=abc",
"html": "<p>Your code is <b>482913</b></p>",
"receivedAt": "2026-10-09T12:00:00Z"
}
]
}
Your response What CertiQA does
200 with a JSON array, or an object with items or messages Uses the newest message by receivedAt. Fields: id, from, subject, body (or text), html, receivedAt (or received). Only body is needed for SMS.
200 with no messages, 204, or 404 Nothing arrived yet. Keeps polling until the step timeout, then fails. It never invents a code.
401 or 403 Fails immediately with an authorization error. Check VIRTUAL_AUTH_HTTP_TOKEN.
3xx redirect Fails with a clear message. Redirects are not followed, so configure the final URL.

The 4-8 digit code and any links are extracted from body (and html) by CertiQA, so your endpoint only has to return the raw message. The first standalone 4-8 digit number wins, so keep other long numbers out of the message body if you control the template.

A complete minimal endpoint, for reference. It receives messages from a mail or SMS webhook at POST /inbound and serves them at GET /messages. It stores everything in memory, so use a real datastore in production.

const http = require('http');
const TOKEN = process.env.TOKEN; // same value you store as VIRTUAL_AUTH_HTTP_TOKEN
const store = []; // { channel, to, subject, body, receivedAt }
http.createServer((req, res) => {
const url = new URL(req.url, 'http://x');
if (req.headers.authorization !== 'Bearer ' + TOKEN) { res.writeHead(401); return res.end(); }
if (req.method === 'POST' && url.pathname === '/inbound') { // point your mail/SMS webhook here
let raw = ''; req.on('data', c => raw += c);
req.on('end', () => { store.push({ ...JSON.parse(raw), receivedAt: new Date().toISOString() }); res.writeHead(204); res.end(); });
return;
}
if (req.method === 'GET' && url.pathname === '/messages') {
const items = store.filter(m => m.channel === url.searchParams.get('channel') && m.to === url.searchParams.get('to'));
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ items }));
}
res.writeHead(404); res.end();
}).listen(8080);