Use Cases

Login & Auth Protection

Defend against credential stuffing, account takeover, and auth abuse

Login & Auth Protection

This guide covers protecting authentication flows from automated attacks including credential stuffing, brute force, and account takeover. Authentication pages are prime targets for bots due to the high value of compromised accounts.

Common Threats

Credential Stuffing

Bots test username/password combinations leaked from other breaches. These attacks use valid credentials, making them harder to detect than brute force.

Brute Force

Systematic attempts to guess passwords, often targeting common passwords or patterns.

Account Takeover (ATO)

Bots that have obtained valid credentials attempt to access and exploit accounts.

Fake Account Creation

Automated registration of accounts for spam, fraud, or abuse.

Integration Strategy

Login Page Protection

BotSigged.init({
  apiKey: 'your-api-key',
  actionThreshold: 60,
  action: 'challenge',
  formProtection: {
    mode: 'holdUntilFormScored',
    maxHoldTime: 3000,
  },
  onHighBotScore: (event) => {
    if (event.level === 'critical') {
      // Log for security team
      securityLog('critical_bot_login_attempt', {
        sessionId: event.sessionId,
        score: event.score,
      });
    }
  },
});

Registration Protection

Stricter settings for account creation:

const registrationConfig = {
  apiKey: 'your-api-key',
  actionThreshold: 50, // Lower threshold
  action: 'challenge',
  challenge: {
    minLevel: 'medium',
    difficulty: 'hard', // Harder challenge for registration
  },
  formProtection: {
    mode: 'holdUntilFormScored',
    maxHoldTime: 5000,
  },
};

Login Flow Protection

Basic Protected Login

async function handleLogin(credentials) {
  const { score, timedOut } = await botSigged.waitUntilReady();

  // Block obvious bots
  if (score && score > 80) {
    throw new Error('Unable to sign in. Please try again later.');
  }

  // Challenge suspicious attempts
  if (score && score > 50) {
    await botSigged.triggerChallenge('high');
  }

  return fetch('/api/auth/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-BotSigged-Session': botSigged.getSessionId(),
    },
    body: JSON.stringify(credentials),
  });
}

Progressive Challenges

Increase difficulty based on failed attempts:

class LoginProtection {
  constructor(botSigged) {
    this.botSigged = botSigged;
    this.failedAttempts = 0;
  }

  async attemptLogin(credentials) {
    const score = this.botSigged.getLastScore()?.bot_score ?? 0;

    // Calculate effective threshold based on failed attempts
    const baseThreshold = 60;
    const threshold = Math.max(30, baseThreshold - this.failedAttempts * 10);

    if (score > threshold) {
      // Harder challenge with more failures
      const difficulty = this.failedAttempts > 2 ? 'critical' : 'high';
      await this.botSigged.triggerChallenge(difficulty);
    }

    try {
      const result = await this.login(credentials);
      this.failedAttempts = 0; // Reset on success
      return result;
    } catch (error) {
      this.failedAttempts++;

      // Lock out after too many failures
      if (this.failedAttempts >= 5) {
        throw new Error('Too many failed attempts. Please try again in 15 minutes.');
      }

      throw error;
    }
  }

  async login(credentials) {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-BotSigged-Session': this.botSigged.getSessionId(),
      },
      body: JSON.stringify(credentials),
    });

    if (!response.ok) {
      throw new Error('Invalid credentials');
    }

    return response.json();
  }
}

Server-Side Verification

// Server-side login handler
async function handleLoginRequest(req, res) {
  const sessionId = req.headers['x-botsigged-session'];
  const { email, password } = req.body;

  // Get session data from BotSigged
  const session = await botSiggedApi.getSession(sessionId);
  const { bot_score, triggered_rules } = session;

  // Log all login attempts for security analysis
  await securityLog.create({
    event: 'login_attempt',
    email,
    sessionId,
    botScore: bot_score,
    triggeredRules: triggered_rules,
    ip: req.ip,
    userAgent: req.headers['user-agent'],
  });

  // Block high-risk attempts
  if (bot_score > 85) {
    await securityLog.create({
      event: 'login_blocked',
      email,
      sessionId,
      reason: 'high_bot_score',
    });

    return res.status(403).json({
      error: 'Unable to sign in. Please contact support.',
    });
  }

  // Validate credentials
  const user = await validateCredentials(email, password);

  if (!user) {
    // Don't reveal if account exists
    return res.status(401).json({
      error: 'Invalid email or password',
    });
  }

  // Flag suspicious successful logins
  if (bot_score > 50) {
    await flagSuspiciousLogin(user.id, sessionId, bot_score);
  }

  // Create session
  const token = await createSession(user);
  return res.json({ token });
}

Registration Protection

Protected Registration Form

async function handleRegistration(userData) {
  const { score } = await botSigged.waitUntilReady();

  // Strict threshold for registration
  if (score && score > 40) {
    // Always challenge for registration
    const result = await botSigged.triggerChallenge('high');

    if (!result.solved) {
      throw new Error('Verification failed. Please try again.');
    }
  }

  return fetch('/api/auth/register', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-BotSigged-Session': botSigged.getSessionId(),
    },
    body: JSON.stringify(userData),
  });
}

Email Verification with Bot Score

Prioritize email verification for suspicious registrations:

// Server-side registration handler
async function handleRegistration(req, res) {
  const sessionId = req.headers['x-botsigged-session'];
  const session = await botSiggedApi.getSession(sessionId);

  // Create user
  const user = await createUser(req.body);

  // Verification strategy based on score
  if (session.bot_score > 50) {
    // Require email verification before any access
    await sendVerificationEmail(user);
    return res.json({
      message: 'Please check your email to verify your account.',
      requiresVerification: true,
    });
  } else {
    // Trusted registration - allow immediate access
    // Still send verification email but don't block
    sendVerificationEmail(user);
    const token = await createSession(user);
    return res.json({ token });
  }
}

Password Reset Protection

Password reset flows are high-value targets:

async function handlePasswordReset(email) {
  const { score } = await botSigged.waitUntilReady();

  // Always challenge password reset requests from suspicious sessions
  if (score && score > 40) {
    await botSigged.triggerChallenge('high');
  }

  // Rate limit on client side too
  const lastReset = localStorage.getItem('lastPasswordReset');
  if (lastReset && Date.now() - parseInt(lastReset) < 60000) {
    throw new Error('Please wait before requesting another reset.');
  }

  localStorage.setItem('lastPasswordReset', Date.now().toString());

  return fetch('/api/auth/reset-password', {
    method: 'POST',
    headers: {
      'X-BotSigged-Session': botSigged.getSessionId(),
    },
    body: JSON.stringify({ email }),
  });
}

Server-side:

async function handlePasswordResetRequest(req, res) {
  const sessionId = req.headers['x-botsigged-session'];
  const { email } = req.body;

  const session = await botSiggedApi.getSession(sessionId);

  // Log all reset requests
  await securityLog.create({
    event: 'password_reset_request',
    email,
    sessionId,
    botScore: session.bot_score,
  });

  // Block high-risk requests
  if (session.bot_score > 70) {
    // Don't reveal if blocked - still show success message
    return res.json({ message: 'If an account exists, you will receive an email.' });
  }

  // Check if account exists
  const user = await findUserByEmail(email);

  if (user) {
    await sendPasswordResetEmail(user);
  }

  // Always return same response to prevent enumeration
  return res.json({ message: 'If an account exists, you will receive an email.' });
}

OAuth/Social Login Protection

Protect OAuth flows:

async function initiateOAuth(provider) {
  const { score } = await botSigged.waitUntilReady();

  // Include session ID in OAuth state for server verification
  const state = JSON.stringify({
    nonce: generateNonce(),
    sessionId: botSigged.getSessionId(),
    botScore: score?.bot_score,
  });

  window.location.href = `/api/auth/${provider}?state=${encodeURIComponent(state)}`;
}

Server callback:

async function handleOAuthCallback(req, res) {
  const state = JSON.parse(req.query.state);
  const { sessionId, botScore } = state;

  // Verify session still exists and score hasn't changed dramatically
  const currentSession = await botSiggedApi.getSession(sessionId);

  if (currentSession.bot_score > 80 || currentSession.bot_score > botScore + 30) {
    // Session became more suspicious
    return res.redirect('/login?error=verification_required');
  }

  // Complete OAuth flow
  const user = await completeOAuthLogin(req.query.code);
  return res.redirect('/dashboard');
}

Auth Provider Integration

Auth0

// Auth0 Action - Post Login
exports.onExecutePostLogin = async (event, api) => {
  const sessionId = event.request.query.botsigged_session;

  if (sessionId) {
    const session = await fetch(`${BOTSIGGED_API}/sessions/${sessionId}`);
    const { bot_score } = await session.json();

    // Add score to token
    api.idToken.setCustomClaim('botsigged_score', bot_score);

    // Block high scores
    if (bot_score > 80) {
      api.access.deny('Access denied due to suspicious activity');
    }
  }
};

Client integration:

import { useAuth0 } from '@auth0/auth0-react';

function LoginButton() {
  const { loginWithRedirect } = useAuth0();
  const { sdk } = useBotSigged();

  const handleLogin = () => {
    loginWithRedirect({
      authorizationParams: {
        botsigged_session: sdk.getSessionId(),
      },
    });
  };

  return <button onClick={handleLogin}>Log In</button>;
}

Clerk

// Clerk middleware
import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware(async (auth, req) => {
  const sessionId = req.headers.get('x-botsigged-session');

  if (sessionId) {
    const session = await fetch(`${BOTSIGGED_API}/sessions/${sessionId}`);
    const { bot_score } = await session.json();

    if (bot_score > 80) {
      return new Response('Forbidden', { status: 403 });
    }
  }
});

NextAuth.js

// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';

export default NextAuth({
  callbacks: {
    async signIn({ user, account, profile, credentials }) {
      // Get session ID from request
      const sessionId = this.req?.headers['x-botsigged-session'];

      if (sessionId) {
        const session = await fetch(`${BOTSIGGED_API}/sessions/${sessionId}`);
        const { bot_score } = await session.json();

        if (bot_score > 80) {
          return false; // Reject sign in
        }
      }

      return true;
    },
  },
});

React Login Component

Complete example:

import { useState } from 'react';
import { useBotSigged } from '@/lib/botsigged';

export function LoginForm() {
  const { sdk, score, isReady } = useBotSigged();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [failedAttempts, setFailedAttempts] = useState(0);

  const isBlocked = score && score.bot_score > 85;
  const needsChallenge = score && score.bot_score > 50;

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError(null);
    setIsSubmitting(true);

    try {
      // Block obvious bots
      if (isBlocked) {
        setError('Unable to sign in. Please contact support.');
        return;
      }

      // Challenge suspicious sessions
      if (needsChallenge || failedAttempts >= 2) {
        const result = await sdk?.triggerChallenge('high');
        if (!result?.solved) {
          setError('Verification failed. Please try again.');
          return;
        }
      }

      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-BotSigged-Session': sdk?.getSessionId() ?? '',
        },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        setFailedAttempts((prev) => prev + 1);

        if (failedAttempts >= 4) {
          setError('Too many failed attempts. Please try again later.');
          return;
        }

        setError('Invalid email or password');
        return;
      }

      // Success - redirect
      const { redirectUrl } = await response.json();
      window.location.href = redirectUrl ?? '/dashboard';
    } catch (err) {
      setError('An error occurred. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="login-form">
      {error && <div className="error">{error}</div>}

      <div className="field">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          autoComplete="email"
        />
      </div>

      <div className="field">
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          autoComplete="current-password"
        />
      </div>

      <button
        type="submit"
        disabled={isSubmitting || !isReady || isBlocked}
      >
        {isSubmitting
          ? 'Signing in...'
          : !isReady
            ? 'Verifying...'
            : 'Sign In'}
      </button>

      <a href="/forgot-password">Forgot password?</a>
    </form>
  );
}

Security Best Practices

Don’t Reveal Information

Never indicate whether an account exists:

// Bad - reveals account exists
if (!user) {
  return { error: 'Account not found' };
}
if (!validPassword) {
  return { error: 'Incorrect password' };
}

// Good - same message for both
if (!user || !validPassword) {
  return { error: 'Invalid email or password' };
}

Constant-Time Comparison

Prevent timing attacks:

import { timingSafeEqual } from 'crypto';

function secureCompare(a: string, b: string): boolean {
  const bufA = Buffer.from(a);
  const bufB = Buffer.from(b);

  if (bufA.length !== bufB.length) {
    // Compare anyway to maintain constant time
    timingSafeEqual(bufA, bufA);
    return false;
  }

  return timingSafeEqual(bufA, bufB);
}

Rate Limit by Score

Apply stricter rate limits for suspicious sessions:

function getRateLimit(botScore: number): number {
  if (botScore > 70) return 3; // 3 attempts per hour
  if (botScore > 50) return 10; // 10 attempts per hour
  return 30; // 30 attempts per hour
}

Log Everything

Comprehensive logging for security analysis:

await securityLog.create({
  event: 'login_attempt',
  email: hashEmail(email), // Hash for privacy
  success: false,
  sessionId,
  botScore: session.bot_score,
  classification: session.classification,
  triggeredRules: session.triggered_rules,
  ip: req.ip,
  userAgent: req.headers['user-agent'],
  timestamp: new Date(),
});