Authentication is a critical aspect of web development. Let's explore modern authentication patterns and best practices:
JWT is a popular choice for stateless authentication:
// Creating a JWT
const jwt = require('jsonwebtoken');
const payload = {
userId: user.id,
email: user.email,
role: user.role
};
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '24h'
});
For third-party authentication:
// Using NextAuth.js
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
})
],
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
}
return token
},
},
})
Modern approach using magic links or OTP:
// Magic link implementation
const sendMagicLink = async (email) => {
const token = crypto.randomBytes(32).toString('hex');
// Store token in database with expiration
await storeLoginToken(email, token, Date.now() + 15 * 60 * 1000);
// Send email with magic link
const magicLink = `${process.env.BASE_URL}/auth/verify?token=${token}`;
await sendEmail(email, 'Magic Link', magicLink);
};
const refreshTokens = async (refreshToken) => {
try {
// Verify refresh token
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
// Generate new tokens
const newAccessToken = generateAccessToken(decoded.userId);
const newRefreshToken = generateRefreshToken(decoded.userId);
// Invalidate old refresh token
await invalidateRefreshToken(refreshToken);
// Store new refresh token
await storeRefreshToken(newRefreshToken, decoded.userId);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
} catch (error) {
throw new Error('Invalid refresh token');
}
};
const sessionOptions = {
password: process.env.SESSION_SECRET,
cookieName: 'app-session',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
},
};
// Using iron-session
import { withIronSessionApiRoute } from 'iron-session/next';
export default withIronSessionApiRoute(
async function handler(req, res) {
const user = req.session.user;
if (!user) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ user });
},
sessionOptions
);
// WebAuthn registration
const registerWebAuthn = async (userId) => {
const challenge = crypto.randomBytes(32);
const publicKeyCredentialCreationOptions = {
challenge,
rp: {
name: 'Your App',
id: 'localhost',
},
user: {
id: new TextEncoder().encode(userId),
name: user.email,
displayName: user.name,
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
authenticatorSelection: {
authenticatorAttachment: 'platform',
userVerification: 'required',
},
timeout: 60000,
attestation: 'direct',
};
return await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions,
});
};
res.setHeader('Set-Cookie', [
`accessToken=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900`,
`refreshToken=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Path=/auth; Max-Age=604800`
]);
const useAuthToken = () => {
const [token, setToken] = useState(null);
useEffect(() => {
const refreshToken = async () => {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setToken(data.accessToken);
}
} catch (error) {
// Redirect to login
window.location.href = '/login';
}
};
// Set up automatic token refresh
const interval = setInterval(refreshToken, 14 * 60 * 1000); // 14 minutes
return () => clearInterval(interval);
}, []);
return token;
};
// TOTP implementation
const speakeasy = require('speakeasy');
const qrcode = require('qrcode');
const setupMFA = async (userId) => {
const secret = speakeasy.generateSecret({
name: `YourApp (${user.email})`,
issuer: 'YourApp',
length: 32
});
// Store secret in database
await storeMFASecret(userId, secret.base32);
// Generate QR code
const qrCodeUrl = await qrcode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32,
qrCode: qrCodeUrl
};
};
const verifyMFA = (token, secret) => {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 1
});
};
const rateLimiter = new Map();
const checkRateLimit = (identifier, maxAttempts = 5, windowMs = 15 * 60 * 1000) => {
const now = Date.now();
const attempts = rateLimiter.get(identifier) || [];
// Remove old attempts outside the window
const recentAttempts = attempts.filter(time => now - time < windowMs);
if (recentAttempts.length >= maxAttempts) {
throw new Error('Too many attempts. Please try again later.');
}
// Add current attempt
recentAttempts.push(now);
rateLimiter.set(identifier, recentAttempts);
};
// Complete authentication service example
class AuthService {
async login(email, password) {
// Rate limiting
await this.checkRateLimit(email);
// Verify credentials
const user = await this.verifyCredentials(email, password);
if (!user) {
throw new Error('Invalid credentials');
}
// Check if MFA is enabled
if (user.mfaEnabled) {
return { requiresMFA: true, tempToken: this.generateTempToken(user.id) };
}
// Generate tokens
const tokens = await this.generateTokens(user);
// Log successful login
await this.logAuthEvent('login', user.id, true);
return tokens;
}
async verifyMFA(tempToken, mfaCode) {
const userId = this.verifyTempToken(tempToken);
const user = await this.getUser(userId);
if (!this.verifyMFACode(mfaCode, user.mfaSecret)) {
throw new Error('Invalid MFA code');
}
return await this.generateTokens(user);
}
async logout(userId) {
await this.invalidateAllTokens(userId);
await this.logAuthEvent('logout', userId, true);
}
}
Choose the authentication method that best fits your application's needs and security requirements. Always prioritize security over convenience, and consider implementing multiple layers of protection.