Skip to content

Commit d3526e1

Browse files
authored
feat: 2fa by email and TOTP (sahat#1545)
1 parent 2d36657 commit d3526e1

File tree

10 files changed

+1066
-58
lines changed

10 files changed

+1066
-58
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ I also tried to make it as **generic** and **reusable** as possible to cover mos
7777
- Profile Details
7878
- Password management (Change, Reset, Forgot)
7979
- Verify Email
80+
- **Two-Factor Authentication (2FA)** Email codes and Authenticator apps
8081
- Link multiple OAuth provider accounts to one account
8182
- Delete Account
8283
- Contact Form (powered by SMTP via Mailgun, AWS SES, etc.)
@@ -635,6 +636,7 @@ Required to run the project before your modifications
635636
| multer | Node.js middleware for handling `multipart/form-data`. |
636637
| nodemailer | Node.js library for sending emails. |
637638
| oauth | OAuth API library without middleware constraints. |
639+
| otpauth | One-Time Password (TOTP/HOTP) library for 2FA authenticator apps. |
638640
| passport | Simple and elegant authentication library for node.js. |
639641
| passport-facebook | Sign-in with Facebook plugin. |
640642
| passport-github2 | Sign-in with GitHub plugin. |

app.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const limiter = rateLimit({
4141
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
4242
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
4343
});
44-
// Strict Auth Rate Limiter Config for signup, password recover, account verification, login by email
44+
// Strict Auth Rate Limiter Config for signup, password recover, account verification, login by email, send 2FA email
4545
const strictLimiter = rateLimit({
4646
windowMs: 60 * 60 * 1000, // 1 hour
4747
max: RATE_LIMIT_STRICT, // attempts per hour
@@ -56,6 +56,15 @@ const loginLimiter = rateLimit({
5656
standardHeaders: true,
5757
legacyHeaders: false,
5858
});
59+
// Login 2FA Rate Limiter Config - allow more requests for 2FA pages per login to avoid UX issues.
60+
// This is after a valid username/password submission, so the attack surface is smaller
61+
// and we want to avoid locking out legitimate users who mistype their 2FA code.
62+
const login2FALimiter = rateLimit({
63+
windowMs: 60 * 60 * 1000, // 1 hour
64+
max: RATE_LIMIT_LOGIN * 5,
65+
standardHeaders: true,
66+
legacyHeaders: false,
67+
});
5968

6069
// This logic for numberOfProxies works for local testing, ngrok use, single host deployments
6170
// behind cloudflare, etc. You may need to change it for more complex network settings.
@@ -205,6 +214,11 @@ app.get('/', homeController.index);
205214
app.get('/login', userController.getLogin);
206215
app.post('/login', loginLimiter, userController.postLogin);
207216
app.get('/login/verify/:token', loginLimiter, userController.getLoginByEmail);
217+
app.get('/login/2fa', login2FALimiter, userController.getTwoFactor);
218+
app.post('/login/2fa', login2FALimiter, userController.postTwoFactor);
219+
app.post('/login/2fa/resend', strictLimiter, userController.resendTwoFactorCode);
220+
app.get('/login/2fa/totp', login2FALimiter, userController.getTotpVerify);
221+
app.post('/login/2fa/totp', login2FALimiter, userController.postTotpVerify);
208222
app.post('/login/webauthn-start', loginLimiter, webauthnController.postLoginStart);
209223
app.get('/login/webauthn-start', (req, res) => res.redirect('/login')); // webauthn-start requires a POST
210224
app.post('/login/webauthn-verify', loginLimiter, webauthnController.postLoginVerify);
@@ -222,6 +236,11 @@ app.get('/account/verify/:token', passportConfig.isAuthenticated, userController
222236
app.get('/account', passportConfig.isAuthenticated, userController.getAccount);
223237
app.post('/account/profile', passportConfig.isAuthenticated, userController.postUpdateProfile);
224238
app.post('/account/password', passportConfig.isAuthenticated, userController.postUpdatePassword);
239+
app.post('/account/2fa/email/enable', passportConfig.isAuthenticated, userController.postEnable2FA);
240+
app.post('/account/2fa/email/remove', passportConfig.isAuthenticated, userController.postRemoveEmail2FA);
241+
app.get('/account/2fa/totp/setup', passportConfig.isAuthenticated, userController.getTotpSetup);
242+
app.post('/account/2fa/totp/setup', passportConfig.isAuthenticated, userController.postTotpSetup);
243+
app.post('/account/2fa/totp/remove', passportConfig.isAuthenticated, userController.postRemoveTotp);
225244
app.post('/account/delete', passportConfig.isAuthenticated, userController.postDeleteAccount);
226245
app.post('/account/logout-everywhere', passportConfig.isAuthenticated, userController.postLogoutEverywhere);
227246
app.get('/account/unlink/:provider', passportConfig.isAuthenticated, userController.getOauthUnlink);

0 commit comments

Comments
 (0)