Skip to main content

Login with 2FA

Use the Trust Score to enforce 2FA only when needed — avoiding friction for clean users while catching bots and anti-detect browser users.

Flow

Browser loads login page
  → checkAnonymous() runs, gets requestId
  → User submits credentials
  → Server checks username/password
  → Server waits for Shield webhook (or checks cached score by requestId)
  → Score < 40: allow login
  → Score 40–69: require 2FA
  → Score 70+: block

Step 1 — Add snippet to login page

<!-- login.html -->
<script type="module">
  const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');
  mod.checkAnonymous((ip, requestId) => {
    document.getElementById('shield-request-id').value = requestId;
  });
</script>

<form method="POST" action="/api/login">
  <input type="hidden" id="shield-request-id" name="shieldRequestId">
  <input type="text"     name="email"    placeholder="Email">
  <input type="password" name="password" placeholder="Password">
  <button type="submit">Login</button>
</form>

Step 2 — Receive and cache the webhook

Store incoming webhooks indexed by RequestID for quick lookup:
// webhook handler
const scoreCache = new Map(); // use Redis in production

app.post('/shieldlabs/webhook', express.json(), (req, res) => {
  const { Data, Assing } = req.body;
  if (!verifyWebhook(Data, Assing, process.env.SHIELD_SECRET)) {
    return res.status(401).end();
  }
  res.status(200).end();

  // Cache score for 5 minutes
  scoreCache.set(Data.RequestID, {
    score:   Data.Score,
    signals: Data.Details,
    ip:      Data.IP,
    os:      Data.OS,
  });
  setTimeout(() => scoreCache.delete(Data.RequestID), 5 * 60 * 1000);
});

Step 3 — Use score at login

app.post('/api/login', async (req, res) => {
  const { email, password, shieldRequestId } = req.body;

  // Verify credentials first
  const user = await verifyCredentials(email, password);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  // Get Shield score (with fallback timeout)
  const shield = await waitForScore(shieldRequestId, 2000); // 2s timeout

  if (!shield || shield.score < 40) {
    // Clean session — log in directly
    return issueSession(res, user);
  }

  if (shield.score < 70) {
    // Elevated risk — require 2FA
    const token = await initiate2FA(user.id);
    return res.json({ requires2FA: true, token });
  }

  // High risk — block
  await logSuspiciousLogin(user.id, shield);
  return res.status(403).json({
    error: 'Login blocked due to suspicious activity',
  });
});

async function waitForScore(requestId, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (scoreCache.has(requestId)) return scoreCache.get(requestId);
    await sleep(100);
  }
  return null;
}

Thresholds for login

ScoreAction
0–39Allow login
40–69Require TOTP / SMS / email OTP
70+Block — log suspicious activity
999Block — rate limit ban

After authentication — force check

For extra protection on already-authenticated users, do a force check at the start of sensitive flows:
// When user initiates a withdrawal or profile change
const mod = await import(`https://cdn.shieldlabs.ai/snippet.js?publicKey=${PUBLIC_KEY}`);
mod.forceCheckAuthenticatedUser(userHashedId, (ip, requestId) => {
  fetch('/api/pre-action', {
    method: 'POST',
    body: JSON.stringify({ requestId, action: 'withdrawal' }),
  });
});