Skip to content

Commit 03229c2

Browse files
authored
feat: Implement passwordless authentication system (#1342)
- Add passwordless login option via email links - Add passwordless signup option with automatic login link - Refactor email verification system for unified security settings - Add IP hash verification to prevent MITM and CSRF attacks - Add token expiration and automatic cleanup via middleware - Add database indexes for token fields - Improve email templates with security instructions - Enhance flash messages for better user feedback - Add token verification checks before processing links - Consolidate email sending logic for maintainability - Add security advisory when emailing auth links - Implement automatic cleanup of expired tokens on save - Add more restrictive rate limiting to auth routes - Add timing-safe token verification to harden against CWE-208 This major update improves security and user experience by adding passwordless authentication while hardening the existing email verification system against common attack vectors.
1 parent 6e656d4 commit 03229c2

File tree

7 files changed

+757
-180
lines changed

7 files changed

+757
-180
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ I also tried to make it as **generic** and **reusable** as possible to cover mos
7070
## Features
7171

7272
- Login
73-
- **Local Authentication** using Email and Password
73+
- **Local Authentication** using Email and Password, as well as Passwordless
7474
- **OAuth 2.0 Authentication:** Sign in with Google, Facebook, X (Twitter), Twitch, Github
7575
- **OpenID Connect:** Sign in with LinkedIn
7676
- **User Profile and Account Management**

app.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,34 @@ dotenv.config({ path: '.env.example' });
2424
*/
2525
const secureTransfer = process.env.BASE_URL.startsWith('https');
2626

27-
// Consider adding a proxy such as cloudflare for production.
27+
/**
28+
* Rate limiting configuration
29+
* This is a basic rate limiting configuration. You may want to adjust the settings
30+
* based on your application's needs and the expected traffic patterns.
31+
* Alos, consider adding a proxy such as cloudflare for production.
32+
*/
33+
// Global Rate Limiter Config
2834
const limiter = rateLimit({
2935
windowMs: 15 * 60 * 1000, // 15 minutes
3036
max: 200, // Limit each IP to 200 requests per `window` (here, per 15 minutes)
3137
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
3238
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
3339
});
40+
// Strict Auth Rate Limiter Config for signup, password recover, account verification, login by email
41+
const strictLimiter = rateLimit({
42+
windowMs: 60 * 60 * 1000, // 1 hour
43+
max: 5, // 5 attempts per hour
44+
standardHeaders: true,
45+
legacyHeaders: false,
46+
});
47+
48+
// Login Rate Limiter Config
49+
const loginLimiter = rateLimit({
50+
windowMs: 60 * 60 * 1000, // 1 hour
51+
max: 10, // 10 attempts per hour
52+
standardHeaders: true,
53+
legacyHeaders: false,
54+
});
3455

3556
// This logic for numberOfProxies works for local testing, ngrok use, single host deployments
3657
// behind cloudflare, etc. You may need to change it for more complex network settings.
@@ -158,15 +179,16 @@ app.locals.FACEBOOK_PIXEL_ID = process.env.FACEBOOK_PIXEL_ID ? process.env.FACEB
158179
*/
159180
app.get('/', homeController.index);
160181
app.get('/login', userController.getLogin);
161-
app.post('/login', userController.postLogin);
182+
app.post('/login', loginLimiter, userController.postLogin);
183+
app.get('/login/verify/:token', loginLimiter, userController.getLoginByEmail);
162184
app.get('/logout', userController.logout);
163185
app.get('/forgot', userController.getForgot);
164-
app.post('/forgot', userController.postForgot);
186+
app.post('/forgot', strictLimiter, userController.postForgot);
165187
app.get('/reset/:token', userController.getReset);
166-
app.post('/reset/:token', userController.postReset);
188+
app.post('/reset/:token', loginLimiter, userController.postReset);
167189
app.get('/signup', userController.getSignup);
168190
app.post('/signup', userController.postSignup);
169-
app.get('/contact', contactController.getContact);
191+
app.get('/contact', strictLimiter, contactController.getContact);
170192
app.post('/contact', contactController.postContact);
171193
app.get('/account/verify', passportConfig.isAuthenticated, userController.getVerifyEmail);
172194
app.get('/account/verify/:token', passportConfig.isAuthenticated, userController.getVerifyEmailToken);
@@ -199,7 +221,7 @@ app.get('/api/paypal/success', apiController.getPayPalSuccess);
199221
app.get('/api/paypal/cancel', apiController.getPayPalCancel);
200222
app.get('/api/lob', apiController.getLob);
201223
app.get('/api/upload', lusca({ csrf: true }), apiController.getFileUpload);
202-
app.post('/api/upload', apiController.uploadMiddleware, lusca({ csrf: true }), apiController.postFileUpload);
224+
app.post('/api/upload', strictLimiter, apiController.uploadMiddleware, lusca({ csrf: true }), apiController.postFileUpload);
203225
app.get('/api/here-maps', apiController.getHereMaps);
204226
app.get('/api/google-maps', apiController.getGoogleMaps);
205227
app.get('/api/google/drive', passportConfig.isAuthenticated, passportConfig.isAuthorized, apiController.getGoogleDrive);

0 commit comments

Comments
 (0)