@dax-crafta/auth 1.3.6 → 2.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A powerful, flexible, and secure authentication system for Node.js applications. Built with enterprise-grade security features while maintaining developer-friendly simplicity.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@dax-crafta/auth.svg)](https://www.npmjs.com/package/@dax-crafta/auth)
6
- [![License](https://img.shields.io/npm/l/@dax-crafta/auth.svg)](https://github.com/daxp472/dax-crafta/auth/blob/main/LICENSE)
6
+ [![License](https://img.shields.io/npm/l/@dax-crafta/auth.svg)](https://github.com/daxp472/crafta/blob/main/LICENSE)
7
7
 
8
8
  ## Features
9
9
 
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@dax-crafta/auth",
3
- "version": "1.3.6",
4
- "description": "Authentication plugin for crafta framework",
3
+ "version": "2.0.0",
4
+ "description": "A powerful, flexible, and secure authentication plugin for the Crafta framework. Supports JWT, social login, 2FA, RBAC, audit logging, and enterprise-grade security features.",
5
5
  "main": "src/index.js",
6
6
  "private": false,
7
7
  "dependencies": {
8
8
  "bcryptjs": "^2.4.3",
9
+ "cookie-parser": "^1.4.7",
10
+ "csurf": "^1.11.0",
11
+ "express-brute": "^1.0.1",
9
12
  "express-rate-limit": "^7.1.4",
10
13
  "google-auth-library": "^9.2.0",
11
14
  "jsonwebtoken": "^9.0.2",
@@ -16,14 +19,41 @@
16
19
  "passport-facebook": "^3.0.0",
17
20
  "passport-github2": "^0.1.12",
18
21
  "passport-google-oauth20": "^2.0.0",
22
+ "qrcode": "^1.5.3",
23
+ "speakeasy": "^2.0.0",
19
24
  "validator": "^13.11.0",
20
- "zxcvbn": "^4.4.2",
21
25
  "winston": "^3.11.0",
22
- "express-brute": "^1.0.1",
23
- "qrcode": "^1.5.3",
24
- "speakeasy": "^2.0.0"
26
+ "zxcvbn": "^4.4.2"
25
27
  },
26
28
  "peerDependencies": {
27
29
  "crafta": "^1.1.4"
28
- }
29
- }
30
+ },
31
+ "keywords": [
32
+ "authentication",
33
+ "auth",
34
+ "jwt",
35
+ "oauth",
36
+ "social-login",
37
+ "role-based-access-control",
38
+ "rbac",
39
+ "2fa",
40
+ "mfa",
41
+ "audit-logging",
42
+ "express",
43
+ "nodejs",
44
+ "crafta",
45
+ "secure-authentication",
46
+ "enterprise-security",
47
+ "user-management"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/daxp472/crafta.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/daxp472/crafta/issues"
55
+ },
56
+ "homepage": "https://github.com/daxp472/crafta#readme",
57
+ "license": "MIT",
58
+ "author": "Dax Patel"
59
+ }
package/src/index.js CHANGED
@@ -1,13 +1,18 @@
1
+ const express = require('express');
1
2
  const passport = require('passport');
3
+ const cookieParser = require('cookie-parser');
4
+ const csrf = require('csurf');
5
+ const helmet = require("helmet");
6
+
2
7
  const GoogleStrategy = require('passport-google-oauth20').Strategy;
3
- const FacebookStrategy = require('passport-facebook').Strategy;
4
- const GitHubStrategy = require('passport-github2').Strategy;
5
8
  const AuthService = require('./services/auth.service');
6
9
  const RoleService = require('./services/role.service');
7
10
  const AuditService = require('./services/audit.service');
8
11
  const MFAService = require('./utils/mfa');
9
12
  const PasswordPolicy = require('./utils/password-policy');
10
13
  const createAuthMiddleware = require('./middlewares/auth.middleware');
14
+ const validators = require('./middlewares/validation.middleware');
15
+
11
16
 
12
17
  const defaultConfig = {
13
18
  strategy: 'jwt',
@@ -40,12 +45,26 @@ const defaultConfig = {
40
45
  google: null,
41
46
  facebook: null,
42
47
  github: null
43
- }
48
+ },
49
+ env: {
50
+ JWT_SECRET: process.env.JWT_SECRET
51
+ },
52
+ accessTokenExpiry: '1h',
53
+ refreshTokenDays: 7
44
54
  };
45
55
 
56
+ // minimal ApiError re-export if needed
57
+ class ApiError extends Error {
58
+ constructor(message, status = 400) {
59
+ super(message);
60
+ this.status = status;
61
+ }
62
+ }
63
+
46
64
  function auth(config = {}) {
47
65
  const finalConfig = { ...defaultConfig, ...config };
48
-
66
+ finalConfig.env = { ...(defaultConfig.env || {}), ...(config.env || process.env) };
67
+
49
68
  if (!finalConfig.env?.JWT_SECRET) {
50
69
  throw new Error('JWT_SECRET is required in environment configuration');
51
70
  }
@@ -54,214 +73,252 @@ function auth(config = {}) {
54
73
  throw new Error('SMTP configuration is required for email verification');
55
74
  }
56
75
 
57
- // Initialize services
58
76
  const authService = new AuthService(finalConfig);
59
77
  const roleService = new RoleService();
60
78
  const auditService = new AuditService();
61
79
  const mfaService = new MFAService();
62
80
  const passwordPolicy = new PasswordPolicy(finalConfig.passwordPolicy);
63
- const { rateLimiter, verifyToken, checkRole } = createAuthMiddleware(finalConfig);
81
+ const { rateLimiter, limiterFor, verifyToken, checkRole, checkOwnershipOrAdmin } = createAuthMiddleware(finalConfig);
64
82
 
65
- // Configure social login strategies
66
- if (finalConfig.social.google) {
83
+ // setup passport strategies as earlier if provided...
84
+ if (finalConfig.social && finalConfig.social.google) {
67
85
  passport.use(new GoogleStrategy(finalConfig.social.google,
68
86
  async (accessToken, refreshToken, profile, done) => {
69
87
  try {
70
88
  const user = await authService.handleSocialLogin('google', profile);
71
89
  done(null, user);
72
- } catch (error) {
73
- done(error);
90
+ } catch (err) {
91
+ done(err);
74
92
  }
75
93
  }
76
94
  ));
77
95
  }
78
96
 
79
- return function(app) {
97
+ return function (app) {
98
+ app.use(cookieParser());
99
+ // CSRF: enable only if config.enableCSRF === true
100
+ if (finalConfig.enableCSRF) {
101
+ // use cookie-based CSRF token and expect header 'x-csrf-token' or cookie
102
+ const csrfProtection = csrf({ cookie: true });
103
+ app.use(csrfProtection);
104
+
105
+ // expose token endpoint to clients (they must fetch this and send it back on state-changing requests)
106
+ app.get('/csrf-token', (req, res) => {
107
+ res.json({ csrfToken: req.csrfToken() });
108
+ });
109
+ }
110
+
80
111
  app.use(passport.initialize());
81
112
  app.use(rateLimiter);
113
+ app.use(helmet({
114
+ contentSecurityPolicy: false, // optional to ease frontend dev
115
+ crossOriginEmbedderPolicy: false,
116
+ }));
82
117
 
83
- // Attach services to app
118
+
119
+ // attach services
84
120
  app.authService = authService;
85
121
  app.roleService = roleService;
86
122
  app.auditService = auditService;
87
123
  app.mfaService = mfaService;
88
124
  app.passwordPolicy = passwordPolicy;
89
125
 
90
- // Register route with password policy
91
- app.post(finalConfig.routes.register, async (req, res) => {
92
- try {
93
- const { isValid, errors } = passwordPolicy.validate(req.body.password, {
94
- email: req.body.email,
95
- name: req.body.name
96
- });
126
+ // register
127
+ app.post(
128
+ finalConfig.routes.register,
129
+ limiterFor('register'),
130
+ validators.registerValidator,
131
+ validators.handleValidation,
132
+ async (req, res, next) => {
133
+ try {
134
+ const { isValid, errors } = passwordPolicy.validate(req.body.password, {
135
+ email: req.body.email,
136
+ name: req.body.name
137
+ });
138
+
139
+ if (!isValid) {
140
+ return res.status(400).json({ success: false, error: 'Password policy violation', details: errors });
141
+ }
97
142
 
98
- if (!isValid) {
99
- return res.status(400).json({
100
- success: false,
101
- errors
143
+ const user = await authService.register(req.body);
144
+ await auditService.logActivity({
145
+ userId: user._id,
146
+ action: 'register',
147
+ ipAddress: req.ip,
148
+ userAgent: req.headers['user-agent'],
149
+ status: 'success'
102
150
  });
151
+
152
+ res.status(201).json({ success: true, message: 'Registration successful' });
153
+ } catch (err) {
154
+ next(err);
103
155
  }
156
+ }
157
+ );
104
158
 
105
- const user = await authService.register(req.body);
106
- await auditService.logActivity({
107
- userId: user._id,
108
- action: 'register',
109
- ipAddress: req.ip,
110
- userAgent: req.headers['user-agent'],
111
- status: 'success'
112
- });
159
+ // login
160
+ app.post(
161
+ finalConfig.routes.login,
162
+ limiterFor('login'),
163
+ validators.loginValidator,
164
+ validators.handleValidation,
165
+ async (req, res, next) => {
166
+ try {
167
+ const deviceInfo = { browser: req.headers['user-agent'], ip: req.ip };
168
+ const result = await authService.login(req.body.email, req.body.password, deviceInfo);
113
169
 
114
- res.status(201).json({
115
- success: true,
116
- message: 'Registration successful'
117
- });
118
- } catch (error) {
119
- res.status(400).json({
120
- success: false,
121
- error: error.message
122
- });
170
+ await auditService.logActivity({
171
+ userId: result.user?._id,
172
+ action: 'login',
173
+ ipAddress: req.ip,
174
+ userAgent: req.headers['user-agent'],
175
+ status: 'success'
176
+ });
177
+
178
+ res.json({ success: true, ...result });
179
+ } catch (err) {
180
+ await auditService.logActivity({
181
+ action: 'login',
182
+ ipAddress: req.ip,
183
+ userAgent: req.headers['user-agent'],
184
+ status: 'failure',
185
+ details: { error: err.message }
186
+ });
187
+ next(err);
188
+ }
123
189
  }
124
- });
190
+ );
125
191
 
126
- // Login route with audit logging and MFA support
127
- app.post(finalConfig.routes.login, async (req, res) => {
128
- try {
129
- const deviceInfo = {
130
- browser: req.headers['user-agent'],
131
- ip: req.ip
132
- };
133
-
134
- const result = await authService.login(
135
- req.body.email,
136
- req.body.password,
137
- deviceInfo
138
- );
139
-
140
- await auditService.logActivity({
141
- userId: result.user?._id,
142
- action: 'login',
143
- ipAddress: req.ip,
144
- userAgent: req.headers['user-agent'],
145
- status: 'success'
146
- });
192
+ // 2FA verification (rate-limited)
193
+ app.post(
194
+ finalConfig.routes.twoFactor,
195
+ limiterFor('2fa'),
196
+ validators.twoFAValidator,
197
+ validators.handleValidation,
198
+ async (req, res, next) => {
199
+ try {
200
+ const result = await authService.verify2FA(req.body.userId, req.body.code);
201
+ await auditService.logActivity({ userId: result.user._id, action: '2fa_verify', status: 'success' });
202
+ res.json({ success: true, ...result });
203
+ } catch (err) {
204
+ next(err);
205
+ }
206
+ }
207
+ );
147
208
 
148
- res.json({
149
- success: true,
150
- ...result
151
- });
152
- } catch (error) {
153
- await auditService.logActivity({
154
- action: 'login',
155
- ipAddress: req.ip,
156
- userAgent: req.headers['user-agent'],
157
- status: 'failure',
158
- details: { error: error.message }
159
- });
209
+ // forgot password (rate-limited)
210
+ app.post(
211
+ finalConfig.routes.forgotPassword,
212
+ limiterFor('forgotPassword'),
213
+ validators.forgotPasswordValidator,
214
+ validators.handleValidation,
215
+ async (req, res, next) => {
216
+ try {
217
+ await authService.forgotPassword(req.body.email);
218
+ res.json({ success: true, message: 'Password reset email sent if user exists' });
219
+ } catch (err) {
220
+ next(err);
221
+ }
222
+ }
223
+ );
160
224
 
161
- res.status(401).json({
162
- success: false,
163
- error: error.message
164
- });
225
+ // reset password
226
+ app.post(
227
+ finalConfig.routes.resetPassword,
228
+ validators.resetPasswordValidator,
229
+ validators.handleValidation,
230
+ async (req, res, next) => {
231
+ try {
232
+ await authService.resetPassword(req.body.token, req.body.newPassword);
233
+ await auditService.logActivity({ action: 'password_reset', status: 'success' });
234
+ res.json({ success: true, message: 'Password reset successful' });
235
+ } catch (err) {
236
+ next(err);
237
+ }
165
238
  }
166
- });
239
+ );
167
240
 
168
- // 2FA verification
169
- app.post(finalConfig.routes.twoFactor, async (req, res) => {
170
- try {
171
- const result = await authService.verify2FA(
172
- req.body.userId,
173
- req.body.code
174
- );
175
-
176
- await auditService.logActivity({
177
- userId: result.user._id,
178
- action: '2fa_verify',
179
- status: 'success'
180
- });
241
+ // refresh token
242
+ app.post(
243
+ finalConfig.routes.refreshToken,
244
+ validators.refreshTokenValidator,
245
+ validators.handleValidation,
246
+ async (req, res, next) => {
247
+ try {
248
+ const result = await authService.refreshToken(req.body.refreshToken);
249
+ res.json({ success: true, ...result });
250
+ } catch (err) {
251
+ next(err);
252
+ }
253
+ }
254
+ );
181
255
 
182
- res.json({
183
- success: true,
184
- ...result
185
- });
186
- } catch (error) {
187
- res.status(401).json({
188
- success: false,
189
- error: error.message
190
- });
256
+ // update profile (prevent IDOR: only owner or admin)
257
+ app.put(
258
+ finalConfig.routes.profile,
259
+ verifyToken,
260
+ checkOwnershipOrAdmin((req) => req.user.id), // target is the calling user; if you allow admin to pass an id param, adapt accordingly
261
+ validators.profileUpdateValidator,
262
+ validators.handleValidation,
263
+ async (req, res, next) => {
264
+ try {
265
+ const user = await authService.updateProfile(req.user.id, req.body);
266
+ await auditService.logActivity({ userId: user._id, action: 'profile_update', status: 'success' });
267
+ res.json({ success: true, user });
268
+ } catch (err) {
269
+ next(err);
270
+ }
191
271
  }
192
- });
272
+ );
193
273
 
194
- // Role management routes
195
- app.post('/roles', verifyToken, checkRole(['admin']), async (req, res) => {
274
+ // roles (admin only)
275
+ app.post('/roles', verifyToken, checkRole(['admin']), async (req, res, next) => {
196
276
  try {
197
277
  const role = await roleService.createRole(req.body);
198
278
  res.status(201).json({ success: true, role });
199
- } catch (error) {
200
- res.status(400).json({
201
- success: false,
202
- error: error.message
203
- });
204
- }
205
- });
206
-
207
- // Update profile with audit logging
208
- app.put(finalConfig.routes.profile, verifyToken, async (req, res) => {
209
- try {
210
- const user = await authService.updateProfile(req.user.id, req.body);
211
- await auditService.logActivity({
212
- userId: user._id,
213
- action: 'profile_update',
214
- status: 'success'
215
- });
216
-
217
- res.json({
218
- success: true,
219
- user
220
- });
221
- } catch (error) {
222
- res.status(400).json({
223
- success: false,
224
- error: error.message
225
- });
279
+ } catch (err) {
280
+ next(err);
226
281
  }
227
282
  });
228
283
 
229
- // Social login routes
230
- if (finalConfig.social.google) {
231
- app.get('/auth/google',
232
- passport.authenticate('google', { scope: ['profile', 'email'] })
233
- );
284
+ // social google callback example (kept same)
285
+ if (finalConfig.social && finalConfig.social.google) {
286
+ app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
234
287
 
235
288
  app.get('/auth/google/callback',
236
289
  passport.authenticate('google', { session: false }),
237
- async (req, res) => {
238
- const { accessToken, refreshToken } = authService.generateTokens(req.user);
239
- await auditService.logActivity({
240
- userId: req.user._id,
241
- action: 'social_login',
242
- status: 'success',
243
- details: { provider: 'google' }
244
- });
245
-
246
- res.json({
247
- success: true,
248
- accessToken,
249
- refreshToken,
250
- user: req.user
251
- });
252
- }
253
- );
290
+ async (req, res, next) => {
291
+ try {
292
+ const tokens = await authService.generateTokens(req.user);
293
+ await auditService.logActivity({
294
+ userId: req.user._id,
295
+ action: 'social_login',
296
+ status: 'success',
297
+ details: { provider: 'google' }
298
+ });
299
+ res.json({ success: true, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, user: req.user });
300
+ } catch (err) {
301
+ next(err);
302
+ }
303
+ });
254
304
  }
255
305
 
256
- // Error handling middleware
306
+ // centralized error handler (friendly for clients)
257
307
  app.use((err, req, res, next) => {
258
- console.error(err.stack);
259
- res.status(500).json({
260
- success: false,
261
- error: 'Internal Server Error'
262
- });
308
+ console.error(err && err.stack ? err.stack : err);
309
+ const status = err.status || (err.name === 'ValidationError' ? 400 : 500);
310
+
311
+ // If client error (4xx) show helpful message, else generic in production
312
+ let message;
313
+ if (status >= 400 && status < 500) {
314
+ message = err.message || 'Bad request';
315
+ } else {
316
+ message = process.env.NODE_ENV === 'development' ? (err.message || 'Internal Server Error') : 'Internal Server Error';
317
+ }
318
+
319
+ res.status(status).json({ success: false, error: message });
263
320
  });
264
321
  };
265
322
  }
266
323
 
267
- module.exports = { auth };
324
+ module.exports = { auth, ApiError };
@@ -1,51 +1,111 @@
1
+ // packages/auth/src/middlewares/auth.middleware.js
1
2
  const rateLimit = require('express-rate-limit');
2
3
  const jwt = require('jsonwebtoken');
3
4
 
4
5
  const createAuthMiddleware = (config) => {
5
- const rateLimiter = rateLimit({
6
+ // Default generic limiter (used for non-sensitive endpoints)
7
+ const genericLimiter = rateLimit({
6
8
  windowMs: 15 * 60 * 1000, // 15 minutes
7
- max: 100 // limit each IP to 100 requests per windowMs
9
+ max: 100,
10
+ standardHeaders: true,
11
+ legacyHeaders: false
8
12
  });
9
13
 
10
- const verifyToken = (req, res, next) => {
11
- const token = req.headers.authorization?.split(' ')[1];
12
-
13
- if (!token) {
14
- return res.status(401).json({
15
- success: false,
16
- error: 'No token provided'
17
- });
14
+ // Sensitive endpoint limiters - tuned for production
15
+ const loginLimiter = rateLimit({
16
+ windowMs: 15 * 60 * 1000,
17
+ max: config?.limits?.loginMax || 10, // default 10 attempts per 15m
18
+ message: { success: false, error: 'Too many login attempts, please try later' },
19
+ standardHeaders: true,
20
+ legacyHeaders: false
21
+ });
22
+
23
+ const twoFALimiter = rateLimit({
24
+ windowMs: 15 * 60 * 1000,
25
+ max: config?.limits?.twoFAMax || 20, // 20 per 15m
26
+ message: { success: false, error: 'Too many 2FA attempts, please try later' },
27
+ standardHeaders: true,
28
+ legacyHeaders: false
29
+ });
30
+
31
+ const forgotPasswordLimiter = rateLimit({
32
+ windowMs: 60 * 60 * 1000,
33
+ max: config?.limits?.forgotPasswordMax || 5, // 5 per hour
34
+ message: { success: false, error: 'Too many password reset requests, please try later' },
35
+ standardHeaders: true,
36
+ legacyHeaders: false
37
+ });
38
+
39
+ // Expose function to choose limiter per-route
40
+ const limiterFor = (routeName) => {
41
+ switch (routeName) {
42
+ case 'login': return loginLimiter;
43
+ case '2fa': return twoFALimiter;
44
+ case 'forgotPassword': return forgotPasswordLimiter;
45
+ case 'refresh': return refreshLimiter;
46
+ default: return genericLimiter;
18
47
  }
48
+ };
49
+
50
+ // Refresh token limiter
51
+ const refreshLimiter = rateLimit({
52
+ windowMs: 15 * 60 * 1000,
53
+ max: config?.limits?.refreshMax || 30,
54
+ message: { success: false, error: 'Too many refresh attempts' }
55
+ });
56
+
19
57
 
58
+ const verifyToken = (req, res, next) => {
59
+ const header = req.headers.authorization;
60
+ if (!header || !header.startsWith('Bearer ')) {
61
+ return res.status(401).json({ success: false, error: 'No token provided' });
62
+ }
63
+ const token = header.split(' ')[1];
20
64
  try {
21
65
  const decoded = jwt.verify(token, config.env.JWT_SECRET);
22
- req.user = decoded;
66
+ // keep minimal user data on req.user
67
+ req.user = { id: decoded.id, role: decoded.role, email: decoded.email };
23
68
  next();
24
- } catch (error) {
25
- res.status(401).json({
26
- success: false,
27
- error: 'Invalid token'
28
- });
69
+ } catch (err) {
70
+ return res.status(401).json({ success: false, error: 'Invalid or expired token' });
29
71
  }
30
72
  };
31
73
 
32
- const checkRole = (roles) => {
74
+ const checkRole = (roles = []) => {
33
75
  return (req, res, next) => {
76
+ if (!req.user) {
77
+ return res.status(401).json({ success: false, error: 'Unauthorized' });
78
+ }
34
79
  if (!roles.includes(req.user.role)) {
35
- return res.status(403).json({
36
- success: false,
37
- error: 'Insufficient permissions'
38
- });
80
+ return res.status(403).json({ success: false, error: 'Insufficient permissions' });
39
81
  }
40
82
  next();
41
83
  };
42
84
  };
43
85
 
86
+ // Ownership check middleware for IDOR protection: allow if owner or admin
87
+ const checkOwnershipOrAdmin = (getTargetUserId) => {
88
+ return async (req, res, next) => {
89
+ try {
90
+ const targetId = typeof getTargetUserId === 'function' ? getTargetUserId(req) : req.params.id;
91
+ if (!req.user) return res.status(401).json({ success: false, error: 'Unauthorized' });
92
+ if (req.user.role === 'admin' || String(req.user.id) === String(targetId)) {
93
+ return next();
94
+ }
95
+ return res.status(403).json({ success: false, error: 'Insufficient permissions to modify this resource' });
96
+ } catch (err) {
97
+ return res.status(500).json({ success: false, error: 'Server error' });
98
+ }
99
+ };
100
+ };
101
+
44
102
  return {
45
- rateLimiter,
103
+ rateLimiter: genericLimiter,
104
+ limiterFor,
46
105
  verifyToken,
47
- checkRole
106
+ checkRole,
107
+ checkOwnershipOrAdmin
48
108
  };
49
109
  };
50
110
 
51
- module.exports = createAuthMiddleware;
111
+ module.exports = createAuthMiddleware;