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(),
});