Learn how to add secure user authentication & registration to your web app with this easy step-by-step guide.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Introduction: Why Authentication Matters
Authentication isn't just a feature—it's the front door to your application. Getting it right means balancing security with user experience. Getting it wrong means potentially exposing user data or creating a frustrating experience that drives users away.
The Complete Authentication Flow
Modern authentication involves several interconnected pieces:
Option 1: Build Your Own (The DIY Approach)
Building your own authentication system gives you complete control but requires careful implementation:
// User registration example (Node.js/Express)
app.post('/register', async (req, res) => {
try {
// Always validate input
const { email, password } = req.body;
// Never store plain text passwords
const hashedPassword = await bcrypt.hash(password, 10); // 10 rounds of salting
// Store user in database
const user = await User.create({
email,
password: hashedPassword
});
// Generate JWT token for immediate login
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
expiresIn: '24h'
});
res.status(201).json({ token });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
DIY Pros and Cons
Option 2: Authentication as a Service (The Ready-Made Approach)
Using a service like Auth0, Firebase Authentication, or Okta can drastically reduce implementation time:
// Firebase Authentication example (JavaScript)
import { initializeApp } from 'firebase/app';
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth';
// Initialize Firebase
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
// other config properties
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// Register a new user
async function registerUser(email, password) {
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
return userCredential.user; // Successfully created user
} catch (error) {
// Handle errors like email-already-in-use, weak-password, etc.
console.error('Registration error:', error.code, error.message);
throw error;
}
}
// Login existing user
async function loginUser(email, password) {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return userCredential.user; // Successfully logged in user
} catch (error) {
console.error('Login error:', error.code, error.message);
throw error;
}
}
Auth Service Pros and Cons
1. Secure Password Storage
Never store passwords in plain text. Always use a strong hashing algorithm with salt:
// Node.js example with bcrypt
const bcrypt = require('bcrypt');
async function hashPassword(plainPassword) {
// The higher the salt rounds, the more secure but slower
const saltRounds = 12;
return await bcrypt.hash(plainPassword, saltRounds);
}
async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
2. Multi-Factor Authentication (MFA)
Adding a second verification layer significantly increases security:
// Time-based OTP verification example
const speakeasy = require('speakeasy');
// When setting up MFA for a user
function setupMFA() {
const secret = speakeasy.generateSecret({ length: 20 });
// Store secret.base32 with the user record
// Return secret.otpauth_url to generate a QR code for the user
return {
secretKey: secret.base32,
otpauthUrl: secret.otpauth_url
};
}
// When verifying a token from the user
function verifyToken(userSecret, userToken) {
return speakeasy.totp.verify({
secret: userSecret,
encoding: 'base32',
token: userToken,
window: 1 // Allows 1 period before and after for clock drift
});
}
3. Session Management
Modern apps typically use JWT (JSON Web Tokens) for managing sessions:
// JWT implementation example
const jwt = require('jsonwebtoken');
// Creating a token after successful login
function generateToken(userId) {
// Never put sensitive data in your JWT payload as it can be decoded
return jwt.sign(
{ userId: userId },
process.env.JWT_SECRET,
{ expiresIn: '7d' } // Token expires in 7 days
);
}
// Middleware to protect routes
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) return res.status(401).json({ error: 'Access denied' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
req.user = user; // Add user info to request
next();
});
}
4. Password Recovery
A secure password reset flow using time-limited tokens:
// Generate a password reset token
async function createPasswordResetToken(userId) {
const resetToken = crypto.randomBytes(32).toString('hex');
// Hash the token before storing it in the database
const hashedToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
await User.findByIdAndUpdate(userId, {
passwordResetToken: hashedToken,
passwordResetExpires: Date.now() + 3600000 // 1 hour
});
return resetToken; // Send this to user's email
}
// Verify and use a password reset token
async function resetPassword(token, newPassword) {
// Hash the received token to compare with stored hash
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gt: Date.now() }
});
if (!user) {
throw new Error('Token is invalid or has expired');
}
// Update password and clear reset token
user.password = await bcrypt.hash(newPassword, 12);
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save();
return true;
}
Creating Intuitive Registration & Login Forms
Design principles for authentication UIs:
// React registration form example with validation
import { useState } from 'react';
function RegistrationForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const validateForm = () => {
let valid = true;
const newErrors = {};
// Email validation
if (!formData.email || !/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
valid = false;
}
// Password validation
if (!formData.password || formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
valid = false;
}
// Password confirmation
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
valid = false;
}
setErrors(newErrors);
return valid;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (validateForm()) {
try {
// API call to register user
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: formData.email,
password: formData.password
})
});
const data = await response.json();
if (response.ok) {
// Success - store token, redirect, etc.
localStorage.setItem('authToken', data.token);
window.location.href = '/dashboard';
} else {
// API returned an error
setErrors({ api: data.error });
}
} catch (error) {
setErrors({ api: 'Registration failed. Please try again.' });
}
}
};
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error-message">{errors.password}</span>}
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
className={errors.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && <span className="error-message">{errors.confirmPassword}</span>}
</div>
{errors.api && <div className="api-error">{errors.api}</div>}
<button type="submit" className="submit-button">Create Account</button>
</form>
);
}
UI/UX Best Practices
Protecting Against Common Attacks
// Express rate limiting example
const rateLimit = require('express-rate-limit');
// Apply to login routes
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per IP
message: 'Too many login attempts. Please try again after 15 minutes.',
standardHeaders: true, // Return rate limit info in RateLimit-* headers
legacyHeaders: false, // Disable X-RateLimit-* headers
});
app.post('/api/login', loginLimiter, loginController.login);
When to Build Your Own
When to Use a Service
Phase 1: Core Authentication
Phase 2: Enhanced Security
Phase 3: Advanced Features
Comprehensive Authentication Testing
// Jest test example for registration endpoint
describe('User Registration', () => {
test('should register a new user with valid credentials', async () => {
const response = await request(app)
.post('/api/register')
.send({
email: '[email protected]',
password: 'SecurePassword123!'
});
expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty('token');
// Verify user was created in database
const user = await User.findOne({ email: '[email protected]' });
expect(user).not.toBeNull();
// Verify password was properly hashed
expect(user.password).not.toBe('SecurePassword123!');
});
test('should reject registration with existing email', async () => {
// Create a user first
await User.create({
email: '[email protected]',
password: await bcrypt.hash('password123', 10)
});
// Try to register with the same email
const response = await request(app)
.post('/api/register')
.send({
email: '[email protected]',
password: 'AnotherPassword123!'
});
expect(response.statusCode).toBe(400);
expect(response.body).toHaveProperty('error');
});
test('should reject registration with weak password', async () => {
const response = await request(app)
.post('/api/register')
.send({
email: '[email protected]',
password: 'weak'
});
expect(response.statusCode).toBe(400);
expect(response.body.error).toContain('password');
});
});
Authentication isn't a set-it-and-forget-it feature. It requires ongoing maintenance as security best practices evolve. Whether you build your own system or use a service, make sure to:
Remember: The best authentication system balances security with usability. Too secure, and users get frustrated. Too simple, and you risk breaches. Finding that balance is the key to successful authentication implementation.
Explore the top 3 essential user authentication and registration use cases for your web app.
A centralized authentication system that allows users to access multiple applications with one set of credentials. This eliminates password fatigue and reduces security risks by consolidating user identity management.
Additional security layers beyond username/password that validate user identity through something they have (phone, email), something they are (biometrics), or somewhere they are (location).
User authentication that includes permission management, restricting system access based on roles assigned to individual users within your organization.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â