@bhandari88/express-auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ # Installation Guide
2
+
3
+ ## Quick Setup
4
+
5
+ ### 1. Install Dependencies
6
+
7
+ ```bash
8
+ npm install
9
+ ```
10
+
11
+ This will install all required dependencies including:
12
+ - Node.js crypto (built-in, no installation needed - used for password hashing with scrypt)
13
+ - jsonwebtoken (JWT tokens)
14
+ - passport (authentication strategies)
15
+ - passport-google-oauth20 (Google login)
16
+ - passport-facebook (Facebook login)
17
+ - validator (input validation)
18
+
19
+ ### 2. Install Peer Dependencies
20
+
21
+ ```bash
22
+ npm install express
23
+ ```
24
+
25
+ ### 3. Build the Package
26
+
27
+ ```bash
28
+ npm run build
29
+ ```
30
+
31
+ This will compile TypeScript to JavaScript in the `dist/` directory.
32
+
33
+ ### 4. Set Environment Variables
34
+
35
+ Create a `.env` file (or use environment variables):
36
+
37
+ ```bash
38
+ JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
39
+ REFRESH_TOKEN_SECRET=your-super-secret-refresh-token-key
40
+ GOOGLE_CLIENT_ID=your-google-client-id
41
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
42
+ FACEBOOK_CLIENT_ID=your-facebook-app-id
43
+ FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
44
+ ```
45
+
46
+ ### 5. Implement User Repository
47
+
48
+ You need to implement the `UserRepository` interface for your database. See `example/usage.ts` for a MongoDB/Mongoose example.
49
+
50
+ ### 6. Use the Package
51
+
52
+ ```typescript
53
+ import { Auth, AuthConfig } from './src';
54
+ // or if installed as package: from '@auth-boiler/express-auth'
55
+
56
+ const auth = new Auth(config, userRepository);
57
+ auth.setupRoutes(app, '/api/auth');
58
+ ```
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ npm run dev # Watch mode for TypeScript compilation
64
+ ```
65
+
66
+ ## Publishing
67
+
68
+ ```bash
69
+ npm run build # Build first
70
+ npm publish # Publish to npm registry
71
+ ```
package/README.md ADDED
@@ -0,0 +1,425 @@
1
+ # @auth-boiler/express-auth
2
+
3
+ A plug-and-play authentication handler for Express.js with TypeScript supporting multiple authentication methods including email, username, phone, and social login (Google, Facebook).
4
+
5
+ ## Features
6
+
7
+ - ✅ **Multiple Authentication Methods**
8
+ - Email/Password
9
+ - Username/Password
10
+ - Phone/Password
11
+ - Social Login (Google, Facebook)
12
+
13
+ - ✅ **Security Best Practices**
14
+ - JWT-based authentication with access and refresh tokens
15
+ - Node.js crypto (scrypt) password hashing (memory-hard, highly secure)
16
+ - Input validation and sanitization
17
+ - Token expiration and refresh mechanism
18
+ - Secure password requirements
19
+ - Timing-safe password comparison to prevent timing attacks
20
+
21
+ - ✅ **Easy Integration**
22
+ - Plug-and-play API
23
+ - Express middleware for protected routes
24
+ - Role-based access control
25
+ - Automatic route setup
26
+ - TypeScript support
27
+
28
+ - ✅ **Flexible**
29
+ - Works with any database/user repository
30
+ - Configurable token expiration
31
+ - Optional refresh tokens
32
+ - Customizable user fields
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install @auth-boiler/express-auth
38
+ ```
39
+
40
+ ## Peer Dependencies
41
+
42
+ Make sure you have the following peer dependencies installed:
43
+
44
+ ```bash
45
+ npm install express
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ### 1. Install Dependencies
51
+
52
+ ```bash
53
+ npm install @auth-boiler/express-auth express
54
+ npm install --save-dev typescript @types/express @types/node
55
+ ```
56
+
57
+ ### 2. Create User Repository
58
+
59
+ Implement the `UserRepository` interface for your database:
60
+
61
+ ```typescript
62
+ import { UserRepository, UserDocument } from '@auth-boiler/express-auth';
63
+
64
+ class MyUserRepository implements UserRepository {
65
+ async findById(id: string): Promise<UserDocument | null> {
66
+ // Your database query logic
67
+ }
68
+
69
+ async findByEmail(email: string): Promise<UserDocument | null> {
70
+ // Your database query logic
71
+ }
72
+
73
+ async findByUsername(username: string): Promise<UserDocument | null> {
74
+ // Your database query logic
75
+ }
76
+
77
+ async findByPhone(phone: string): Promise<UserDocument | null> {
78
+ // Your database query logic
79
+ }
80
+
81
+ async findByProvider(provider: string, providerId: string): Promise<UserDocument | null> {
82
+ // Your database query logic
83
+ }
84
+
85
+ async create(userData: Partial<UserDocument>): Promise<UserDocument> {
86
+ // Your database create logic
87
+ }
88
+
89
+ async update(id: string, updateData: Partial<UserDocument>): Promise<UserDocument | null> {
90
+ // Your database update logic
91
+ }
92
+
93
+ async delete(id: string): Promise<boolean> {
94
+ // Your database delete logic
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### 3. Configure and Initialize Auth
100
+
101
+ ```typescript
102
+ import express from 'express';
103
+ import { Auth, AuthConfig } from '@auth-boiler/express-auth';
104
+ import MyUserRepository from './repositories/MyUserRepository';
105
+
106
+ const app = express();
107
+ app.use(express.json());
108
+
109
+ const authConfig: AuthConfig = {
110
+ jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
111
+ jwtExpiresIn: '15m',
112
+ refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET || 'your-refresh-secret',
113
+ refreshTokenExpiresIn: '7d',
114
+ bcryptRounds: 16384, // Scrypt cost parameter (2^14, configurable)
115
+ enableRefreshTokens: true,
116
+ socialAuth: {
117
+ google: {
118
+ clientID: process.env.GOOGLE_CLIENT_ID || '',
119
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
120
+ callbackURL: 'http://localhost:3000/api/auth/google/callback',
121
+ },
122
+ facebook: {
123
+ clientID: process.env.FACEBOOK_CLIENT_ID || '',
124
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET || '',
125
+ callbackURL: 'http://localhost:3000/api/auth/facebook/callback',
126
+ },
127
+ },
128
+ };
129
+
130
+ const userRepository = new MyUserRepository();
131
+ const auth = new Auth(authConfig, userRepository);
132
+
133
+ // Setup all auth routes automatically
134
+ auth.setupRoutes(app, '/api/auth');
135
+
136
+ app.listen(3000, () => {
137
+ console.log('Server running on port 3000');
138
+ });
139
+ ```
140
+
141
+ ### 4. Use Protected Routes
142
+
143
+ ```typescript
144
+ // Protect a route
145
+ app.get('/api/profile', auth.getAuthMiddleware(), (req, res) => {
146
+ const user = (req as any).user;
147
+ res.json({ user });
148
+ });
149
+
150
+ // Optional authentication (doesn't fail if no token)
151
+ app.get('/api/public', auth.getOptionalAuthMiddleware(), (req, res) => {
152
+ const user = (req as any).user; // May be undefined
153
+ res.json({ user });
154
+ });
155
+
156
+ // Role-based access control
157
+ app.get('/api/admin',
158
+ auth.getAuthMiddleware(),
159
+ auth.requireRole(['admin', 'superadmin']),
160
+ (req, res) => {
161
+ res.json({ message: 'Admin access granted' });
162
+ }
163
+ );
164
+ ```
165
+
166
+ ## API Endpoints
167
+
168
+ ### Register User
169
+
170
+ **POST** `/api/auth/register`
171
+
172
+ ```json
173
+ {
174
+ "email": "user@example.com",
175
+ "password": "securepassword123"
176
+ }
177
+ ```
178
+
179
+ Or with username:
180
+ ```json
181
+ {
182
+ "username": "johndoe",
183
+ "password": "securepassword123"
184
+ }
185
+ ```
186
+
187
+ Or with phone:
188
+ ```json
189
+ {
190
+ "phone": "+1234567890",
191
+ "password": "securepassword123"
192
+ }
193
+ ```
194
+
195
+ **Response:**
196
+ ```json
197
+ {
198
+ "success": true,
199
+ "user": {
200
+ "id": "user-id",
201
+ "email": "user@example.com",
202
+ "verified": false
203
+ },
204
+ "tokens": {
205
+ "accessToken": "jwt-token",
206
+ "refreshToken": "refresh-token",
207
+ "expiresIn": 900
208
+ },
209
+ "message": "User registered successfully"
210
+ }
211
+ ```
212
+
213
+ ### Login
214
+
215
+ **POST** `/api/auth/login`
216
+
217
+ ```json
218
+ {
219
+ "email": "user@example.com",
220
+ "password": "securepassword123"
221
+ }
222
+ ```
223
+
224
+ Or with username:
225
+ ```json
226
+ {
227
+ "username": "johndoe",
228
+ "password": "securepassword123"
229
+ }
230
+ ```
231
+
232
+ Or with phone:
233
+ ```json
234
+ {
235
+ "phone": "+1234567890",
236
+ "password": "securepassword123"
237
+ }
238
+ ```
239
+
240
+ **Response:**
241
+ ```json
242
+ {
243
+ "success": true,
244
+ "user": {
245
+ "id": "user-id",
246
+ "email": "user@example.com"
247
+ },
248
+ "tokens": {
249
+ "accessToken": "jwt-token",
250
+ "refreshToken": "refresh-token",
251
+ "expiresIn": 900
252
+ },
253
+ "message": "Login successful"
254
+ }
255
+ ```
256
+
257
+ ### Refresh Token
258
+
259
+ **POST** `/api/auth/refresh`
260
+
261
+ ```json
262
+ {
263
+ "refreshToken": "your-refresh-token"
264
+ }
265
+ ```
266
+
267
+ **Response:**
268
+ ```json
269
+ {
270
+ "success": true,
271
+ "tokens": {
272
+ "accessToken": "new-jwt-token",
273
+ "refreshToken": "new-refresh-token",
274
+ "expiresIn": 900
275
+ },
276
+ "message": "Token refreshed successfully"
277
+ }
278
+ ```
279
+
280
+ ### Change Password
281
+
282
+ **POST** `/api/auth/change-password`
283
+
284
+ **Headers:**
285
+ ```
286
+ Authorization: Bearer <access-token>
287
+ ```
288
+
289
+ **Body:**
290
+ ```json
291
+ {
292
+ "oldPassword": "oldpassword123",
293
+ "newPassword": "newpassword456"
294
+ }
295
+ ```
296
+
297
+ ### Social Login
298
+
299
+ **Google:**
300
+ - **GET** `/api/auth/google` - Initiate Google login
301
+ - **GET** `/api/auth/google/callback` - Google callback (configured automatically)
302
+
303
+ **Facebook:**
304
+ - **GET** `/api/auth/facebook` - Initiate Facebook login
305
+ - **GET** `/api/auth/facebook/callback` - Facebook callback (configured automatically)
306
+
307
+ ## Programmatic Usage
308
+
309
+ You can also use the auth methods programmatically:
310
+
311
+ ```typescript
312
+ // Register
313
+ const result = await auth.register({
314
+ email: 'user@example.com',
315
+ password: 'password123',
316
+ });
317
+
318
+ // Login
319
+ const loginResult = await auth.login({
320
+ email: 'user@example.com',
321
+ password: 'password123',
322
+ });
323
+
324
+ // Change password
325
+ const changeResult = await auth.changePassword(
326
+ userId,
327
+ 'oldPassword',
328
+ 'newPassword'
329
+ );
330
+
331
+ // Refresh token
332
+ const refreshResult = await auth.refreshToken(refreshToken);
333
+ ```
334
+
335
+ ## Configuration Options
336
+
337
+ ```typescript
338
+ interface AuthConfig {
339
+ jwtSecret: string; // Required: Secret for JWT signing
340
+ jwtExpiresIn?: string; // Optional: Access token expiration (default: '15m')
341
+ refreshTokenSecret?: string; // Optional: Secret for refresh tokens (defaults to jwtSecret)
342
+ refreshTokenExpiresIn?: string; // Optional: Refresh token expiration (default: '7d')
343
+ bcryptRounds?: number; // Optional: Scrypt cost parameter N (default: 16384 = 2^14)
344
+ enableRefreshTokens?: boolean; // Optional: Enable refresh tokens (default: true)
345
+ socialAuth?: { // Optional: Social auth configuration
346
+ google?: {
347
+ clientID: string;
348
+ clientSecret: string;
349
+ callbackURL: string;
350
+ };
351
+ facebook?: {
352
+ clientID: string;
353
+ clientSecret: string;
354
+ callbackURL: string;
355
+ };
356
+ };
357
+ }
358
+ ```
359
+
360
+ ## Security Best Practices
361
+
362
+ 1. **Environment Variables**: Always use environment variables for secrets:
363
+ ```bash
364
+ JWT_SECRET=your-super-secret-key
365
+ REFRESH_TOKEN_SECRET=your-refresh-secret-key
366
+ GOOGLE_CLIENT_ID=your-google-client-id
367
+ GOOGLE_CLIENT_SECRET=your-google-client-secret
368
+ ```
369
+
370
+ 2. **Password Requirements**:
371
+ - Minimum 6 characters
372
+ - Maximum 128 characters
373
+ - Hashed with Node.js crypto.scrypt (memory-hard, highly secure)
374
+ - Uses timing-safe comparison to prevent timing attacks
375
+ - Default cost parameter: 16384 (2^14)
376
+
377
+ 3. **Token Expiration**:
378
+ - Access tokens: Short-lived (15 minutes default)
379
+ - Refresh tokens: Long-lived (7 days default)
380
+
381
+ 4. **HTTPS**: Always use HTTPS in production
382
+
383
+ 5. **Input Validation**: All inputs are validated and sanitized automatically
384
+
385
+ ## Types
386
+
387
+ ```typescript
388
+ interface User {
389
+ id: string;
390
+ email?: string;
391
+ username?: string;
392
+ phone?: string;
393
+ password?: string;
394
+ provider?: 'local' | 'google' | 'facebook';
395
+ providerId?: string;
396
+ verified?: boolean;
397
+ [key: string]: any; // Additional custom fields
398
+ }
399
+
400
+ interface AuthResult {
401
+ success: boolean;
402
+ user?: User;
403
+ tokens?: AuthTokens;
404
+ message?: string;
405
+ error?: string;
406
+ }
407
+
408
+ interface AuthTokens {
409
+ accessToken: string;
410
+ refreshToken?: string;
411
+ expiresIn: number;
412
+ }
413
+ ```
414
+
415
+ ## Example with MongoDB (Mongoose)
416
+
417
+ See `example/usage.ts` for a complete example using MongoDB with Mongoose.
418
+
419
+ ## License
420
+
421
+ MIT
422
+
423
+ ## Contributing
424
+
425
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,2 @@
1
+ export { LocalAuthHandler } from './local-auth';
2
+ export { SocialAuthHandler } from './social-auth';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SocialAuthHandler = exports.LocalAuthHandler = void 0;
4
+ var local_auth_1 = require("./local-auth");
5
+ Object.defineProperty(exports, "LocalAuthHandler", { enumerable: true, get: function () { return local_auth_1.LocalAuthHandler; } });
6
+ var social_auth_1 = require("./social-auth");
7
+ Object.defineProperty(exports, "SocialAuthHandler", { enumerable: true, get: function () { return social_auth_1.SocialAuthHandler; } });
@@ -0,0 +1,12 @@
1
+ import { UserRepository, LoginCredentials, RegisterData, AuthResult } from '../types';
2
+ import { JwtService } from '../utils/jwt';
3
+ import { PasswordService } from '../utils/password';
4
+ export declare class LocalAuthHandler {
5
+ private userRepository;
6
+ private jwtService;
7
+ private passwordService;
8
+ constructor(userRepository: UserRepository, jwtService: JwtService, passwordService: PasswordService);
9
+ register(data: RegisterData): Promise<AuthResult>;
10
+ login(credentials: LoginCredentials): Promise<AuthResult>;
11
+ changePassword(userId: string, oldPassword: string, newPassword: string): Promise<AuthResult>;
12
+ }
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalAuthHandler = void 0;
4
+ const validator_1 = require("../utils/validator");
5
+ class LocalAuthHandler {
6
+ constructor(userRepository, jwtService, passwordService) {
7
+ this.userRepository = userRepository;
8
+ this.jwtService = jwtService;
9
+ this.passwordService = passwordService;
10
+ }
11
+ async register(data) {
12
+ try {
13
+ // Validate input
14
+ const validation = validator_1.ValidatorService.validateRegisterData(data);
15
+ if (!validation.valid) {
16
+ return { success: false, error: validation.message };
17
+ }
18
+ // Check if user already exists
19
+ let existingUser = null;
20
+ if (data.email) {
21
+ const sanitizedEmail = validator_1.ValidatorService.sanitizeEmail(data.email);
22
+ existingUser = await this.userRepository.findByEmail(sanitizedEmail);
23
+ if (existingUser) {
24
+ return { success: false, error: 'User with this email already exists' };
25
+ }
26
+ }
27
+ if (data.username) {
28
+ existingUser = await this.userRepository.findByUsername(data.username);
29
+ if (existingUser) {
30
+ return { success: false, error: 'Username already taken' };
31
+ }
32
+ }
33
+ if (data.phone) {
34
+ const sanitizedPhone = validator_1.ValidatorService.sanitizePhone(data.phone);
35
+ existingUser = await this.userRepository.findByPhone(sanitizedPhone);
36
+ if (existingUser) {
37
+ return { success: false, error: 'User with this phone number already exists' };
38
+ }
39
+ }
40
+ // Validate password
41
+ const passwordValidation = this.passwordService.validatePassword(data.password);
42
+ if (!passwordValidation.valid) {
43
+ return { success: false, error: passwordValidation.message };
44
+ }
45
+ // Hash password
46
+ const hashedPassword = await this.passwordService.hash(data.password);
47
+ // Create user
48
+ const userData = {
49
+ password: hashedPassword,
50
+ provider: 'local',
51
+ verified: false,
52
+ };
53
+ if (data.email) {
54
+ userData.email = validator_1.ValidatorService.sanitizeEmail(data.email);
55
+ }
56
+ if (data.username) {
57
+ userData.username = data.username.toLowerCase().trim();
58
+ }
59
+ if (data.phone) {
60
+ userData.phone = validator_1.ValidatorService.sanitizePhone(data.phone);
61
+ }
62
+ // Add any additional fields
63
+ const { email, username, phone, password, ...additionalFields } = data;
64
+ Object.assign(userData, additionalFields);
65
+ const user = await this.userRepository.create(userData);
66
+ // Generate tokens
67
+ const tokens = this.jwtService.generateTokens({
68
+ userId: user.id || user._id || '',
69
+ email: user.email,
70
+ username: user.username,
71
+ phone: user.phone,
72
+ });
73
+ // Remove password from response
74
+ const { password: _, ...userWithoutPassword } = user;
75
+ const userResponse = userWithoutPassword;
76
+ return {
77
+ success: true,
78
+ user: userResponse,
79
+ tokens,
80
+ message: 'User registered successfully',
81
+ };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ success: false,
86
+ error: error instanceof Error ? error.message : 'Registration failed',
87
+ };
88
+ }
89
+ }
90
+ async login(credentials) {
91
+ try {
92
+ // Validate input
93
+ const validation = validator_1.ValidatorService.validateLoginCredentials(credentials);
94
+ if (!validation.valid) {
95
+ return { success: false, error: validation.message };
96
+ }
97
+ // Find user
98
+ let user = null;
99
+ if (credentials.email) {
100
+ const sanitizedEmail = validator_1.ValidatorService.sanitizeEmail(credentials.email);
101
+ user = await this.userRepository.findByEmail(sanitizedEmail);
102
+ }
103
+ else if (credentials.username) {
104
+ user = await this.userRepository.findByUsername(credentials.username);
105
+ }
106
+ else if (credentials.phone) {
107
+ const sanitizedPhone = validator_1.ValidatorService.sanitizePhone(credentials.phone);
108
+ user = await this.userRepository.findByPhone(sanitizedPhone);
109
+ }
110
+ if (!user) {
111
+ return { success: false, error: 'Invalid credentials' };
112
+ }
113
+ // Check if user has password (not social login only)
114
+ if (!user.password) {
115
+ return { success: false, error: 'Please use social login for this account' };
116
+ }
117
+ // Verify password
118
+ const isPasswordValid = await this.passwordService.compare(credentials.password, user.password);
119
+ if (!isPasswordValid) {
120
+ return { success: false, error: 'Invalid credentials' };
121
+ }
122
+ // Generate tokens
123
+ const tokens = this.jwtService.generateTokens({
124
+ userId: user.id || user._id || '',
125
+ email: user.email,
126
+ username: user.username,
127
+ phone: user.phone,
128
+ });
129
+ // Remove password from response
130
+ const { password: _, ...userWithoutPassword } = user;
131
+ const userResponse = userWithoutPassword;
132
+ return {
133
+ success: true,
134
+ user: userResponse,
135
+ tokens,
136
+ message: 'Login successful',
137
+ };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ success: false,
142
+ error: error instanceof Error ? error.message : 'Login failed',
143
+ };
144
+ }
145
+ }
146
+ async changePassword(userId, oldPassword, newPassword) {
147
+ try {
148
+ const user = await this.userRepository.findById(userId);
149
+ if (!user) {
150
+ return { success: false, error: 'User not found' };
151
+ }
152
+ if (!user.password) {
153
+ return { success: false, error: 'Password change not available for social login accounts' };
154
+ }
155
+ // Verify old password
156
+ const isOldPasswordValid = await this.passwordService.compare(oldPassword, user.password);
157
+ if (!isOldPasswordValid) {
158
+ return { success: false, error: 'Current password is incorrect' };
159
+ }
160
+ // Validate new password
161
+ const passwordValidation = this.passwordService.validatePassword(newPassword);
162
+ if (!passwordValidation.valid) {
163
+ return { success: false, error: passwordValidation.message };
164
+ }
165
+ // Hash new password
166
+ const hashedPassword = await this.passwordService.hash(newPassword);
167
+ // Update user
168
+ await this.userRepository.update(userId, { password: hashedPassword });
169
+ return {
170
+ success: true,
171
+ message: 'Password changed successfully',
172
+ };
173
+ }
174
+ catch (error) {
175
+ return {
176
+ success: false,
177
+ error: error instanceof Error ? error.message : 'Password change failed',
178
+ };
179
+ }
180
+ }
181
+ }
182
+ exports.LocalAuthHandler = LocalAuthHandler;