@touchque/node
v3.1.0
Published
Official Node.js SDK for TouchQue Authenticator (2FA/MFA, passkeys, adaptive auth)
Maintainers
Readme
@touchque/node
The official Node.js server SDK for TouchQue — biometric push 2FA, passkeys, and offline approval codes, added to any backend with one line per route.
📘 Full docs: authenticator.touchque.com/docs
Install
npm install @touchque/nodeSetup
Get an API key and secret from your TouchQue Dashboard, then set them as environment variables:
TQ_API_KEY=tq_auth_your_key
TQ_API_SECRET=your_api_secretThe client picks these up automatically — new TouchQue() with no arguments.
Quick start (Express)
import express from 'express';
import { requireTouchQue } from '@touchque/node';
const app = express();
app.use(express.json());
app.post('/transfer',
requireTouchQue('SEND_MONEY', {
details: (req) => ({ Amount: `${req.body.amount} EUR`, To: req.body.iban }),
}),
(req, res) => {
// Only reached once the user approved on their phone.
res.json({ ok: true, assurance: req.touchque.assurance });
}
);
app.listen(3000);That's the whole integration for one route. What actually happens:
- The first request comes in with no pending approval → the middleware calls
TouchQue, sends a push to the user's phone, and answers
202 { touchque, token }instead of running your handler. - Your frontend shows
touchquein its own UI — a matching number for the user to tap on their phone, or a QR code the first time they link the app — then sends the same request again with headerX-TouchQue-Token: <token>(see@touchque/web, which does this loop for you). - Once the user approves, that retried request reaches your handler exactly
once, with
req.touchquepopulated (assurance,approvalProof, …).
No hosted page, no redirect, no new domain — you keep your own UI end to end.
The three primitives, if you don't use a framework adapter
import { TouchQue } from '@touchque/node';
const tq = new TouchQue(); // from TQ_API_KEY / TQ_API_SECRET
const step = await tq.start('SEND_MONEY', {
user: '[email protected]',
details: { Amount: '250 EUR', To: 'DE89...' },
});
// step.state: 'waiting' (show step.number) | 'enroll' (show step.enroll.qrCodeDataUrl)
// | 'approved' | 'rejected' | 'expired' | 'passkey_required' | 'frozen' | 'blocked'
const latest = await tq.check(step.id);
// Once approved, consume it exactly once, right before doing the protected thing:
const approval = await tq.complete(step.id, {
user: '[email protected]',
action: 'SEND_MONEY',
details: { Amount: '250 EUR', To: 'DE89...' },
});complete() verifies the approval was actually issued for this user, action
and transaction — it will not let an approval for a different amount or
recipient be replayed against this call, and consuming it twice fails on the
second call.
Framework adapters
- Express:
requireTouchQue(action, options?)— shown above. - Next.js / Fetch API route handlers:
withTouchQue(action, options?). touchqueRouter: mounts every relay route a frontend needs (POST /login, enrollment, passkey ceremonies, offline codes) so you don't hand-write them.
import { touchqueRouter } from '@touchque/node';
app.use(touchqueRouter(tq, {
getUserId: (req) => req.session.user?.email,
// The user who already passed your password step. Binds POST /login and the
// offline routes to them; the offline routes stay off (403) without it.
getLoginUser: (req) => req.session.passwordVerifiedUser,
}));Passkeys (phishing-resistant)
Push approval and offline codes stop password reuse and push fatigue, but a real-time phishing proxy can still relay them. A passkey can't be phished: the browser signs your site's real origin and TouchQue refuses any other (NIST SP 800-63B-4 §3.2.5).
- Set your passkey domain in the Dashboard (Security Policy → Passkeys).
- Let users register one via
tq.webauthn(server) +@touchque/web'spasskeys.register()(browser). - Optionally require it for critical actions or every sign-in — TouchQue then
skips the push and your route resolves
passkey_requiredinstead. - Check
assurance.phishingResistantbefore treating a session as high-assurance.
Offline approval (no internet on the phone)
const ch = await tq.offline.challenge({
user: '[email protected]',
type: 'WITHDRAW',
details: { Amount: '1,250.00 USD', Recipient: 'Jane Doe' },
});
// show ch.qrDataUrl — the phone scans it offline and shows a 7-character code
const { approved } = await tq.offline.verify({ challengeId: ch.challengeId, code });A QR that follows a push. Pass the push's requestId when the offline QR is the fallback for a
push the user already started (requireTouchQue / touchqueRouter do this for you):
const ch = await tq.offline.challenge({ externalUsername, type: 'LOGIN', requestId: step.requestId });
// ch.challengeCode is the number to print under the QR when number matching applies.- If the user rejects the push on the phone, the offline QR dies with it: no new QR is issued for that
sign-in (
409 request_rejected), a code for a QR already on screen is refused (reason: 'request_rejected') and so is the time-based code (verifyTotp({ …, requestId })). Treat it as a final "no". - With number matching, the page prints
ch.challengeCodeunder the QR; the phone shows it among two decoys after scanning and the user taps the one that matches. The phone is never told which is right — a wrong tap produces a code that fails verification.
Webhooks
app.post('/webhooks/touchque', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = tq.webhook.verify({
rawBody: req.body.toString(),
signature: req.headers['x-touchque-signature'] as string,
});
// handle event.event: 'login.confirmed' | 'login.rejected' | …
res.sendStatus(200);
} catch {
res.sendStatus(403); // not from TouchQue
}
});Errors
All SDK errors extend TouchQueError: TouchQueAPIError, TouchQueNetworkError,
TouchQueRejectedError, TouchQueTimeoutError, TouchQueWebhookSignatureError,
TouchQueConfigError, TouchQuePasskeyRequiredError.
Security
- Every API request is signed HMAC-SHA256 (method, path+query, timestamp, nonce, body hash).
- The
X-TouchQue-Tokena frontend echoes back is itself signed and bound to one user + action + transaction digest — it can't be replayed for a different amount, recipient or user. - An approval is consumed exactly once, server-side.
- Your API secret never leaves your server.
See SECURITY.md to report a vulnerability.
Requirements
- Node.js 18+
- A TouchQue Dashboard account
License
MIT © TouchQue
