Add KEYRA Partner 2FA to an existing login
Default path: Node.js / TypeScript. Switch tabs for Java or REST. All secret calls stay on your server.
On this page
Prerequisites
- KEYRA developer account and project
projectId, secretclientId(cp_*), andclientSecret(sk_*) — see API Keys & Credentials- A stable application user id to use as
externalUserId - Node.js 18+ for the TypeScript examples (or Java 11+ / plain HTTP)
Installation
npm install git+https://github.com/Ciright-Inc/keyra-typescript-sdk.git qrcodeqrcode is not part of KEYRA — it encodes the URL string returned as qrCode / enrollmentUrl / challengeUrl.
Environment variables
Env names are developer-defined examples. The SDKs accept config objects/constructors — they do not require these exact names.
KEYRA_BASE_URL=https://auth.keyra.ie
KEYRA_PROJECT_ID=
KEYRA_CLIENT_ID=
KEYRA_CLIENT_SECRET=Initialize KEYRA
import { createKeyraPartner2FA } from "@keyra/typescript-sdk";
// Env var names below are examples — the SDK takes config values directly.
export const keyra = createKeyraPartner2FA({
baseUrl: process.env.KEYRA_BASE_URL ?? "https://auth.keyra.ie",
projectId: process.env.KEYRA_PROJECT_ID!, // UUID from Developer Portal
clientId: process.env.KEYRA_CLIENT_ID!, // cp_test_… or cp_prod_…
clientSecret: process.env.KEYRA_CLIENT_SECRET!, // sk_test_… or sk_prod_…
});What happens here? Your backend constructs a Partner 2FA client that signs every request with Bearer clientId:clientSecret and scopes operations to projectId.
Check enrollment
const status = await keyra.get2FAStatus(user.id);
if (!status.enrolled) {
// Run enrollment flow
} else {
// Run authentication challenge flow
}{
"enrolled": true,
"enabled": true,
"identityId": "kid_…",
"status": "active",
"factors": [{ "type": "phone_otp", "verifiedAt": "2026-08-01T12:00:00.000Z" }],
"enrolledAt": "2026-08-01T12:00:00.000Z"
}Not enrolled → enrollment flow. Enrolled → authentication challenge flow.
Enroll the user
import QRCode from "qrcode";
const enrollment = await keyra.enable2FA(user.id);
// enrollment.qrCode === enrollment.enrollmentUrl (URL string — encode to QR yourself)
const qrDataUrl = await QRCode.toDataURL(enrollment.qrCode);
// Send qrDataUrl to your login UI, then wait:
const done = await keyra.waitForEnrollment(enrollment.enrollmentId);
// done.terminal === "COMPLETED" when successful{
"identityId": "kid_…",
"enrollmentId": "…",
"status": "pending",
"expiresIn": 600,
"enrollmentUrl": "https://get-started.keyra.ie/?enroll=…",
"pollUrl": "/v1/identities/enroll/…/status"
}Start authentication & wait
const challenge = await keyra.startAuthentication(user.id);
// challenge.qrCode === challenge.challengeUrl
const qrDataUrl = await QRCode.toDataURL(challenge.qrCode);
const approved = await keyra.waitForChallengeApproval(challenge.challengeId);
if (!approved.verificationToken) {
throw new Error("Missing verificationToken — do not poll again before consume");
}
const result = await keyra.consumeChallenge(
challenge.challengeId,
approved.verificationToken,
);
if (result.consumed) {
// Create YOUR application session here (cookie/JWT/etc).
// KEYRA does not create your app session.
}| Field | Meaning |
|---|---|
challengeId | Id for poll + consume — reuse this id; do not create a second challenge. |
challengeUrl / qrCode | URL to show as QR / open on device. |
expiresIn | Seconds until expiry (typically 120). |
pollAfterMs | Suggested poll delay from API (typically 1500). |
verificationToken | Present on approved poll (once) — required for consume. |
Defaults: waitForChallengeApproval timeout 120s, interval 1.5s (uses pollAfterMs when returned). Terminal failures throw KeyraChallengeDeniedError / KeyraChallengeExpiredError / KeyraTimeoutError.
Consume verification & create your session
{
"consumed": true,
"challengeId": "…",
"identityId": "kid_…"
}Complete Node.js example
Illustrative Express routes. Adapt session storage to your stack. Critical: /auth/challenge/wait reuses the challengeId from startAuthentication — it must not call startAuthentication again.
import express from "express";
import QRCode from "qrcode";
import { createKeyraPartner2FA } from "@keyra/typescript-sdk";
const keyra = createKeyraPartner2FA({
baseUrl: process.env.KEYRA_BASE_URL ?? "https://auth.keyra.ie",
projectId: process.env.KEYRA_PROJECT_ID!,
clientId: process.env.KEYRA_CLIENT_ID!,
clientSecret: process.env.KEYRA_CLIENT_SECRET!,
});
const app = express();
app.use(express.json());
/** After your primary password check succeeds */
app.post("/auth/login/complete-2fa", async (req, res) => {
const externalUserId = String(req.body.userId ?? "");
if (!externalUserId) return res.status(400).json({ error: "userId required" });
try {
const status = await keyra.get2FAStatus(externalUserId);
if (!status.enrolled) {
const enrollment = await keyra.enable2FA(externalUserId);
const qrDataUrl = await QRCode.toDataURL(enrollment.qrCode);
// Client should display QR, then call /auth/enrollment/wait
return res.json({
action: "enroll",
enrollmentId: enrollment.enrollmentId,
qrDataUrl,
expiresIn: enrollment.expiresIn,
});
}
const challenge = await keyra.startAuthentication(externalUserId);
const qrDataUrl = await QRCode.toDataURL(challenge.qrCode);
// Client displays QR; then call /auth/challenge/wait with THIS challengeId
return res.json({
action: "challenge",
challengeId: challenge.challengeId,
qrDataUrl,
expiresIn: challenge.expiresIn,
});
} catch (err) {
console.error(err);
return res.status(500).json({ error: "keyra_error" });
}
});
app.post("/auth/enrollment/wait", async (req, res) => {
const enrollmentId = String(req.body.enrollmentId ?? "");
try {
const done = await keyra.waitForEnrollment(enrollmentId);
return res.json({ terminal: done.terminal, identityId: done.identityId });
} catch (err) {
console.error(err);
return res.status(408).json({ error: "enrollment_timeout_or_failed" });
}
});
app.post("/auth/challenge/wait", async (req, res) => {
const challengeId = String(req.body.challengeId ?? "");
try {
// Reuse the same challengeId from startAuthentication — do NOT start a new challenge here.
const approved = await keyra.waitForChallengeApproval(challengeId);
if (!approved.verificationToken) {
return res.status(409).json({ error: "missing_verification_token" });
}
const result = await keyra.consumeChallenge(challengeId, approved.verificationToken);
if (!result.consumed) {
return res.status(401).json({ error: "not_consumed" });
}
// Example only — replace with your real session strategy.
req.session = req.session ?? {};
(req.session as { userId?: string }).userId = String(req.body.userId);
return res.json({ ok: true, identityId: result.identityId });
} catch (err) {
console.error(err);
return res.status(401).json({ error: "challenge_failed" });
}
});