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.
The INPUT_TOTP Step Action
Section titled “The INPUT_TOTP Step Action”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:
Real Inbound Email Codes & Magic Links
Section titled “Real Inbound Email Codes & Magic Links”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 (
Bring Your Own Email/SMS Endpoint
Section titled “Bring Your Own Email/SMS Endpoint”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 |
| 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@ |
| 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": "noreply@yourapp.com", "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_TOKENconst 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);