Authentication Challenges
After enrollment, each login or step-up creates a short-lived challenge the user must approve.
On this page
YOUR LOGIN UI YOUR BACKEND KEYRA USER DEVICE
│ │ │ │
│ Credentials │ │ │
├───────────────────►│ │ │
│ │ get2FAStatus │ │
│ ├─────────────────►│ │
│ │ enrolled: true │ │
│ │◄─────────────────┤ │
│ │ startAuthentication │
│ ├─────────────────►│ │
│ │ challengeUrl │ │
│ │◄─────────────────┤ │
│ Show QR │ │ │
│◄───────────────────┤ │ │
│ │ │◄──── open/scan ─────┤
│ │ │──── user verifies ──►│
│ │ waitForChallengeApproval │
│ ├─────────────────►│ │
│ │ approved + token │ │
│ │◄─────────────────┤ │
│ │ consumeChallenge │ │
│ ├─────────────────►│ │
│ │ consumed: true │ │
│ │◄─────────────────┤ │
│ │ Create YOUR app session │
│ Authenticated │ │ │
│◄───────────────────┤ │ │Prerequisites
- Identity enrolled (
get2FAStatus→enrolled: true/ statusactive). - Secret Partner credentials on your backend.
If not enrolled: HTTP 404 not_enrolled — run enrollment first.
Start a challenge
const challenge = await keyra.startAuthentication(externalUserId, {
nonce: optionalOpaqueString, // max 256 chars
returnUrl: optionalHttpsUrl, // must satisfy project callback rules
});responseResponse
{
"challengeId": "…",
"status": "pending",
"expiresIn": 120,
"challengeUrl": "https://get-started.keyra.ie/challenge/…",
"pollAfterMs": 1500
}Challenge lifetime at creation: 120 seconds. SDK adds qrCode as an alias of challengeUrl.
Display the challenge
import QRCode from "qrcode";
const qrDataUrl = await QRCode.toDataURL(challenge.qrCode);
// Browser: <img src={qrDataUrl} alt="KEYRA verification QR" />
// Keep challenge.challengeId on the server/session for wait + consume.What happens here? Your backend creates the challenge; KEYRA hosts the user verification UI at challengeUrl; your UI only presents that URL as QR or link.
Wait for approval
// Recommended helper (timeout 120s, interval 1.5s; honors pollAfterMs)
const approved = await keyra.waitForChallengeApproval(challenge.challengeId);
// Manual polling
const snap = await keyra.pollChallenge(challenge.challengeId);
// When status === "approved", read verificationToken ONCE, then consume.Defaults: timeout 120s, interval 1500ms.
Terminal states
| Status | Meaning | SDK behavior |
|---|---|---|
approved | User verified; token may be present | waitForChallengeApproval resolves |
denied | User denied | throws KeyraChallengeDeniedError |
expired | Timer elapsed while pending/scanned/otp_sent | throws KeyraChallengeExpiredError |
consumed | Already consumed | Do not consume again |
Intermediate: pending, scanned, otp_sent.
Next step
Approval alone is not application login. Continue to consume / verification.
