@bhandari88/express-auth
v1.2.0
Published
Plug-and-play authentication handler for Express.js with TypeScript supporting email, username, phone, and social login
Maintainers
Readme
@bhandari88/express-auth
A plug-and-play authentication handler for Express.js with TypeScript supporting multiple authentication methods including email, username, phone, and social login (Google, Facebook).
📦 Package
npm install @bhandari88/express-authPackage URL: https://www.npmjs.com/package/@bhandari88/express-auth
Example
*Example Application: https://github.com/manojsinghindiit/auth-boiler-example
Features
✅ Multiple Authentication Methods
- Email/Password
- Username/Password
- Phone/Password
- Social Login (Google, Facebook)
✅ Security Best Practices
- JWT-based authentication with access and refresh tokens
- Node.js crypto (scrypt) password hashing (memory-hard, highly secure)
- Input validation and sanitization
- Token expiration and refresh mechanism
- Secure password requirements
- Timing-safe password comparison to prevent timing attacks
✅ Easy Integration
- Plug-and-play API
- Express middleware for protected routes
- Request data validation middleware with custom messages
- Role-based access control
- Automatic route setup
- TypeScript support
✅ Flexible
- Works with any database/user repository
- Configurable token expiration
- Optional refresh tokens
- Customizable user fields
Peer Dependencies
Make sure you have the following peer dependencies installed:
npm install expressQuick Start
1. Install Dependencies
npm install @bhandari88/express-auth express
npm install --save-dev typescript @types/express @types/node2. Create User Repository
Implement the UserRepository interface for your database:
import { UserRepository, UserDocument } from '@bhandari88/express-auth';
class MyUserRepository implements UserRepository {
async findById(id: string): Promise<UserDocument | null> {
// Your database query logic
}
async findByEmail(email: string): Promise<UserDocument | null> {
// Your database query logic
}
async findByUsername(username: string): Promise<UserDocument | null> {
// Your database query logic
}
async findByPhone(phone: string): Promise<UserDocument | null> {
// Your database query logic
}
async findByProvider(provider: string, providerId: string): Promise<UserDocument | null> {
// Your database query logic
}
async create(userData: Partial<UserDocument>): Promise<UserDocument> {
// Your database create logic
}
async update(id: string, updateData: Partial<UserDocument>): Promise<UserDocument | null> {
// Your database update logic
}
async delete(id: string): Promise<boolean> {
// Your database delete logic
}
}3. Configure and Initialize Auth
import express from 'express';
import { Auth, AuthConfig } from '@bhandari88/express-auth';
import MyUserRepository from './repositories/MyUserRepository';
const app = express();
app.use(express.json());
const authConfig: AuthConfig = {
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
jwtExpiresIn: '15m',
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET || 'your-refresh-secret',
refreshTokenExpiresIn: '7d',
bcryptRounds: 16384, // Scrypt cost parameter (2^14, configurable)
enableRefreshTokens: true,
socialAuth: {
google: {
clientID: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
callbackURL: 'http://localhost:3000/api/auth/google/callback',
},
facebook: {
clientID: process.env.FACEBOOK_CLIENT_ID || '',
clientSecret: process.env.FACEBOOK_CLIENT_SECRET || '',
callbackURL: 'http://localhost:3000/api/auth/facebook/callback',
},
},
};
const userRepository = new MyUserRepository();
const auth = new Auth(authConfig, userRepository);
// Setup all auth routes automatically
auth.setupRoutes(app, '/api/auth');
app.listen(3000, () => {
console.log('Server running on port 3000');
});4. Use Protected Routes
// Protect a route
app.get('/api/profile', auth.getAuthMiddleware(), (req, res) => {
const user = (req as any).user;
res.json({ user });
});
// Optional authentication (doesn't fail if no token)
app.get('/api/public', auth.getOptionalAuthMiddleware(), (req, res) => {
const user = (req as any).user; // May be undefined
res.json({ user });
});
// Role-based access control
app.get('/api/admin',
auth.getAuthMiddleware(),
auth.requireRole(['admin', 'superadmin']),
(req, res) => {
res.json({ message: 'Admin access granted' });
}
);5. Use Validation Middleware
The package includes a powerful validation middleware for request data validation with custom error messages. No third-party dependencies required!
Basic Usage
import { validate, validateBody, validateQuery, validateParams } from '@bhandari88/express-auth';
// Validate request body
app.post('/api/users', validateBody({
email: {
rules: [
{ type: 'required', message: 'Email is required' },
{ type: 'email', message: 'Invalid email format' }
]
},
password: {
rules: [
{ type: 'required', message: 'Password is required' },
{ type: 'minLength', value: 8, message: 'Password must be at least 8 characters' }
]
},
age: {
rules: [
{ type: 'number', message: 'Age must be a number' },
{ type: 'min', value: 18, message: 'Age must be at least 18' },
{ type: 'max', value: 120, message: 'Age must be at most 120' }
]
}
}), (req, res) => {
// req.body is validated
res.json({ message: 'User created', data: req.body });
});Shorthand Syntax
You can also use a simpler array syntax:
app.post('/api/register', validateBody({
username: [
{ type: 'required', message: 'Username is required' },
{ type: 'minLength', value: 3, message: 'Username must be at least 3 characters' },
{ type: 'maxLength', value: 30, message: 'Username must be at most 30 characters' },
{ type: 'pattern', value: /^[a-zA-Z0-9_-]+$/, message: 'Username can only contain letters, numbers, underscores, and hyphens' }
],
email: [
{ type: 'required' },
{ type: 'email', message: 'Invalid email address' }
]
}), handler);Validate Query Parameters
app.get('/api/users', validateQuery({
page: [
{ type: 'number', message: 'Page must be a number' },
{ type: 'min', value: 1, message: 'Page must be at least 1' }
],
limit: [
{ type: 'number' },
{ type: 'min', value: 1 },
{ type: 'max', value: 100, message: 'Limit cannot exceed 100' }
],
status: [
{ type: 'enum', values: ['active', 'inactive', 'pending'], message: 'Status must be active, inactive, or pending' }
]
}), (req, res) => {
// req.query is validated
res.json({ users: [] });
});Validate Route Parameters
app.get('/api/users/:id', validateParams({
id: [
{ type: 'required', message: 'User ID is required' },
{ type: 'pattern', value: /^[a-f\d]{24}$/i, message: 'Invalid user ID format' }
]
}), (req, res) => {
// req.params is validated
res.json({ user: {} });
});Validate Multiple Sources
app.put('/api/users/:id', validate({
id: [
{ type: 'required' },
{ type: 'pattern', value: /^[a-f\d]{24}$/i }
],
email: [
{ type: 'email' }
],
status: [
{ type: 'enum', values: ['active', 'inactive'] }
]
}, {
source: ['params', 'body'] // Validate both params and body
}), handler);Custom Validation
app.post('/api/custom', validateBody({
password: [
{ type: 'required' },
{ type: 'minLength', value: 8 },
{
type: 'custom',
validator: (value) => {
// Custom validation logic
return /[A-Z]/.test(value) && /[a-z]/.test(value) && /[0-9]/.test(value);
},
message: 'Password must contain uppercase, lowercase, and number'
}
],
confirmPassword: [
{ type: 'required' },
{
type: 'custom',
validator: (value, data) => {
// Access other fields from the request
return value === data.password;
},
message: 'Passwords do not match'
}
]
}), handler);Available Validation Rules
| Rule | Description | Example |
|------|-------------|---------|
| required | Field must be present and not empty | { type: 'required', message: 'Field is required' } |
| string | Value must be a string | { type: 'string' } |
| number | Value must be a number | { type: 'number' } |
| boolean | Value must be a boolean | { type: 'boolean' } |
| email | Value must be a valid email | { type: 'email', message: 'Invalid email' } |
| min | Number must be >= value | { type: 'min', value: 1 } |
| max | Number must be <= value | { type: 'max', value: 100 } |
| minLength | String length must be >= value | { type: 'minLength', value: 8 } |
| maxLength | String length must be <= value | { type: 'maxLength', value: 255 } |
| pattern | String must match regex | { type: 'pattern', value: /^[a-z]+$/ } |
| enum | Value must be in array | { type: 'enum', values: ['a', 'b', 'c'] } |
| array | Value must be an array | { type: 'array' } |
| object | Value must be an object | { type: 'object' } |
| custom | Custom validation function | { type: 'custom', validator: (val) => boolean } |
Validation Options
validate(schema, {
source: 'body' | 'query' | 'params' | ['body', 'query'], // What to validate (default: 'body')
abortEarly: boolean // Stop on first error or collect all errors (default: false)
})Error Response Format
When validation fails, the middleware returns a 400 status with:
{
"success": false,
"error": "Validation failed",
"errors": {
"email": "Invalid email format",
"password": "Password must be at least 8 characters"
}
}Or with abortEarly: true, only the first error:
{
"success": false,
"error": "Validation failed",
"errors": {
"email": "Email is required"
}
}API Endpoints
Register User
POST /api/auth/register
{
"email": "[email protected]",
"password": "securepassword123"
}Or with username:
{
"username": "johndoe",
"password": "securepassword123"
}Or with phone:
{
"phone": "+1234567890",
"password": "securepassword123"
}Response:
{
"success": true,
"user": {
"id": "user-id",
"email": "[email protected]",
"verified": false
},
"tokens": {
"accessToken": "jwt-token",
"refreshToken": "refresh-token",
"expiresIn": 900
},
"message": "User registered successfully"
}Login
POST /api/auth/login
{
"email": "[email protected]",
"password": "securepassword123"
}Or with username:
{
"username": "johndoe",
"password": "securepassword123"
}Or with phone:
{
"phone": "+1234567890",
"password": "securepassword123"
}Response:
{
"success": true,
"user": {
"id": "user-id",
"email": "[email protected]"
},
"tokens": {
"accessToken": "jwt-token",
"refreshToken": "refresh-token",
"expiresIn": 900
},
"message": "Login successful"
}Refresh Token
POST /api/auth/refresh
{
"refreshToken": "your-refresh-token"
}Response:
{
"success": true,
"tokens": {
"accessToken": "new-jwt-token",
"refreshToken": "new-refresh-token",
"expiresIn": 900
},
"message": "Token refreshed successfully"
}Change Password
POST /api/auth/change-password
Headers:
Authorization: Bearer <access-token>Body:
{
"oldPassword": "oldpassword123",
"newPassword": "newpassword456"
}Social Login
Google:
- GET
/api/auth/google- Initiate Google login - GET
/api/auth/google/callback- Google callback (configured automatically)
Facebook:
- GET
/api/auth/facebook- Initiate Facebook login - GET
/api/auth/facebook/callback- Facebook callback (configured automatically)
Programmatic Usage
You can also use the auth methods programmatically:
// Register
const result = await auth.register({
email: '[email protected]',
password: 'password123',
});
// Login
const loginResult = await auth.login({
email: '[email protected]',
password: 'password123',
});
// Change password
const changeResult = await auth.changePassword(
userId,
'oldPassword',
'newPassword'
);
// Refresh token
const refreshResult = await auth.refreshToken(refreshToken);Configuration Options
interface AuthConfig {
jwtSecret: string; // Required: Secret for JWT signing
jwtExpiresIn?: string; // Optional: Access token expiration (default: '15m')
refreshTokenSecret?: string; // Optional: Secret for refresh tokens (defaults to jwtSecret)
refreshTokenExpiresIn?: string; // Optional: Refresh token expiration (default: '7d')
bcryptRounds?: number; // Optional: Scrypt cost parameter N (default: 16384 = 2^14)
enableRefreshTokens?: boolean; // Optional: Enable refresh tokens (default: true)
socialAuth?: { // Optional: Social auth configuration
google?: {
clientID: string;
clientSecret: string;
callbackURL: string;
};
facebook?: {
clientID: string;
clientSecret: string;
callbackURL: string;
};
};
}Security Best Practices
Environment Variables: Always use environment variables for secrets:
JWT_SECRET=your-super-secret-key REFRESH_TOKEN_SECRET=your-refresh-secret-key GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secretPassword Requirements:
- Minimum 6 characters
- Maximum 128 characters
- Hashed with Node.js crypto.scrypt (memory-hard, highly secure)
- Uses timing-safe comparison to prevent timing attacks
- Default cost parameter: 16384 (2^14)
Token Expiration:
- Access tokens: Short-lived (15 minutes default)
- Refresh tokens: Long-lived (7 days default)
HTTPS: Always use HTTPS in production
Input Validation: All inputs are validated and sanitized automatically
Types
interface User {
id: string;
email?: string;
username?: string;
phone?: string;
password?: string;
provider?: 'local' | 'google' | 'facebook';
providerId?: string;
verified?: boolean;
[key: string]: any; // Additional custom fields
}
interface AuthResult {
success: boolean;
user?: User;
tokens?: AuthTokens;
message?: string;
error?: string;
}
interface AuthTokens {
accessToken: string;
refreshToken?: string;
expiresIn: number;
}Example with MongoDB (Mongoose)
See example/usage.ts for a complete example using MongoDB with Mongoose.
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
