From 1463bd4d6fcb43cec27625e04d20024756831619 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 3 Mar 2022 11:55:38 -0500 Subject: [PATCH] email verififcation and OTP/2FA --- app/controllers/user.js | 61 ++++++++++++++++++--- app/services/otp-auth.js | 8 +++ app/services/user.js | 6 +-- app/views/user/otp-disabled.pug | 8 +++ app/views/user/otp-setup-complete.pug | 8 +++ app/views/user/settings.pug | 78 ++++++++++++++++++++++----- client/less/site/image.less | 2 +- config/limiter.js | 10 ++++ 8 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 app/views/user/otp-disabled.pug create mode 100644 app/views/user/otp-setup-complete.pug diff --git a/app/controllers/user.js b/app/controllers/user.js index 071b7c9..19c5996 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -31,6 +31,12 @@ class UserController extends SiteController { dtp.app.use('/user', router); const authRequired = sessionService.authCheckMiddleware({ requireLogin: true }); + + const otpSetup = otpAuthService.middleware('Account', { + adminRequired: false, + otpRequired: true, + otpRedirectURL: async (req) => { return `/user/${req.user._id}`; }, + }); const otpMiddleware = otpAuthService.middleware('Account', { adminRequired: false, otpRequired: false, @@ -58,31 +64,52 @@ class UserController extends SiteController { router.param('userId', this.populateUser.bind(this)); - router.post('/:userId/profile-photo', + router.post( + '/:userId/profile-photo', limiterService.create(limiterService.config.user.postProfilePhoto), + checkProfileOwner, upload.single('imageFile'), this.postProfilePhoto.bind(this), ); - router.post('/:userId/settings', + router.post( + '/:userId/settings', limiterService.create(limiterService.config.user.postUpdateSettings), + checkProfileOwner, upload.none(), this.postUpdateSettings.bind(this), ); - router.post('/', + router.post( + '/', limiterService.create(limiterService.config.user.postCreate), this.postCreateUser.bind(this), ); - router.get('/:userId/settings', + router.get( + '/:userId/otp-setup', + limiterService.create(limiterService.config.user.getOtpSetup), + otpSetup, + this.getOtpSetup.bind(this), + ); + router.get( + '/:userId/otp-disable', + limiterService.create(limiterService.config.user.getOtpDisable), + authRequired, + this.getOtpDisable.bind(this), + ); + + + router.get( + '/:userId/settings', limiterService.create(limiterService.config.user.getSettings), authRequired, otpMiddleware, checkProfileOwner, this.getUserSettingsView.bind(this), ); - router.get('/:userId', + router.get( + '/:userId', limiterService.create(limiterService.config.user.getUserProfile), authRequired, otpMiddleware, @@ -90,7 +117,8 @@ class UserController extends SiteController { this.getUserView.bind(this), ); - router.delete('/:userId/profile-photo', + router.delete( + '/:userId/profile-photo', limiterService.create(limiterService.config.user.deleteProfilePhoto), authRequired, checkProfileOwner, @@ -214,8 +242,29 @@ class UserController extends SiteController { } } + async getOtpSetup (req, res) { + res.render('user/otp-setup-complete'); + } + + async getOtpDisable (req, res) { + const { otpAuth: otpAuthService } = this.dtp.services; + try { + await otpAuthService.destroyOtpSession(req, 'Account'); + await otpAuthService.removeForUser(req.user, 'Account'); + res.render('user/otp-disabled'); + } catch (error) { + this.log.error('failed to disable OTP service for Account', { error }); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } + async getUserSettingsView (req, res, next) { + const { otpAuth: otpAuthService } = this.dtp.services; try { + res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account'); res.locals.startTab = req.query.st || 'watch'; res.render('user/settings'); } catch (error) { diff --git a/app/services/otp-auth.js b/app/services/otp-auth.js index 60731fd..d3b723d 100644 --- a/app/services/otp-auth.js +++ b/app/services/otp-auth.js @@ -213,6 +213,14 @@ class OtpAuthService extends SiteService { return true; } + async isUserProtected (user, serviceName) { + const account = await OtpAccount.findOne({ user: user._id, service: serviceName }); + if (!account) { + return false; + } + return true; + } + async removeForUser (user) { return await OtpAccount.deleteMany({ user: user }); } diff --git a/app/services/user.js b/app/services/user.js index e8e030a..2a274a2 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -277,7 +277,7 @@ class UserService { { username_lc: accountUsername }, ] }) - .select('+passwordSalt +password +flags') + .select('+passwordSalt +password +flags +optIn +permissions') .lean(); if (!user) { throw new SiteError(404, 'Member credentials are invalid'); @@ -371,7 +371,7 @@ class UserService { async getUserAccount (userId) { const user = await User .findById(userId) - .select('+email +flags +permissions +picture') + .select('+email +flags +permissions +optIn +picture') .populate(this.populateUser) .lean(); if (!user) { @@ -388,7 +388,7 @@ class UserService { const users = await User .find(search) .sort({ username_lc: 1 }) - .select('+email +flags +permissions') + .select('+email +flags +permissions +optIn') .skip(pagination.skip) .limit(pagination.cpp) .lean() diff --git a/app/views/user/otp-disabled.pug b/app/views/user/otp-disabled.pug new file mode 100644 index 0000000..41d7989 --- /dev/null +++ b/app/views/user/otp-disabled.pug @@ -0,0 +1,8 @@ +extends ../layouts/main +block content + + section.uk-section.uk-section-default + .uk-container.uk-text-center + h1 Two-Factor Authentication + p You have successfully disabled Two-Factor Authentication. + a(href=`/user/${user._id}/settings?tab=account`).uk-button.dtp-button-default Return to Settings \ No newline at end of file diff --git a/app/views/user/otp-setup-complete.pug b/app/views/user/otp-setup-complete.pug new file mode 100644 index 0000000..294483c --- /dev/null +++ b/app/views/user/otp-setup-complete.pug @@ -0,0 +1,8 @@ +extends ../layouts/main +block content + + section.uk-section.uk-section-default + .uk-container.uk-text-center + h1 Two-Factor Authentication + p You have successfully enabled Two-Factor Authentication on your account. + a(href=`/user/${user._id}/settings?tab=account`).uk-button.dtp-button-default Return to Settings \ No newline at end of file diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 6351911..306bc70 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -7,10 +7,8 @@ block content include ../components/file-upload-image - section.uk-section.uk-section-default + section.uk-section.uk-section-default.uk-section-small .uk-container - h1 Settings - div(uk-grid) div(class="uk-width-1-1 uk-width-1-3@m") - @@ -19,17 +17,34 @@ block content currentProfile = user.picture.large; } .uk-margin - +renderFileUploadImage( - `/user/${user._id}/profile-photo`, - 'profile-picture-upload', - 'profile-picture-file', - 'site-profile-picture', - `/img/default-member.png`, - currentProfile, - { aspectRatio: 1 }, - ) + .uk-card.uk-card-default.uk-card-small + .uk-card-header.uk-text-center + h1.uk-card-title Profile Photo + .uk-card-body + +renderFileUploadImage( + `/user/${user._id}/profile-photo`, + 'profile-picture-upload', + 'profile-picture-file', + 'site-profile-picture', + `/img/default-member.png`, + currentProfile, + { aspectRatio: 1 }, + ) + + .uk-margin + .uk-card.uk-card-default.uk-card-small + .uk-card-header.uk-text-center + h1.uk-card-title Two-Factor Authentication + .uk-card-body + p Enabling Two-Factor Authentication (2FA) with a One-Time Password authenticator app can help protect your account and settings from unauthorized access. + .uk-card-footer.uk-text-center + if hasOtpAccount + a(href=`/user/${user._id}/otp-disable`).uk-button.dtp-button-danger Disable 2FA + else + a(href=`/user/${user._id}/otp-setup`).uk-button.dtp-button-default Enable 2FA div(class="uk-width-1-1 uk-width-expand@m") + h1 Settings form(method="POST", action=`/user/${user._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form .uk-margin label(for="username").uk-form-label Username @@ -38,5 +53,44 @@ block content label(for="display-name").uk-form-label Display Name input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= user.displayName).uk-input + .uk-margin + div(uk-grid).uk-grid-small + .uk-width-1-2 + .uk-margin + label(for="password").uk-form-label New Password + input(id="password", name="password", type="password", placeholder="Enter new password", autocomplete= "new-password").uk-input + .uk-width-1-2 + .uk-margin + label(for="passwordv").uk-form-label Verify New Password + input(id="passwordv", name="passwordv", type="password", placeholder="Enter new password again", autocomplete= "new-password").uk-input + + fieldset + legend Email Preferences + + .uk-margin + label(for="email").uk-form-label + span Email Address + if user.flags.isEmailVerified + span (verified) + div(uk-grid).uk-grid-small + div(class="uk-width-1-1 uk-width-expand@s") + .uk-margin-small + input(id="email", name="email", type="email", placeholder="Enter email address", value= user.email).uk-input + if user.flags.isEmailVerified + .uk-text-small.uk-text-muted Changing your email address will un-verify you and send a new verification email. Check your spam folder! + else + .uk-text-small.uk-text-muted Changing your email address will send a new verification email. Check your spam folder! + div(class="uk-width-1-1 uk-width-auto@s") + button(type="button", onclick="return dtp.app.resendWelcomeEmail(event);").uk-button.dtp-button-default Resend Welcome Email + + .uk-margin + div(uk-grid).uk-grid-small + label + input(id="optin-system", name="optIn.system", type="checkbox", checked= user.optIn ? user.optIn.system : false).uk-checkbox + | System messages + label + input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= user.optIn ? user.optIn.marketing : false).uk-checkbox + | Sales / Marketing + .uk-margin button(type="submit").uk-button.dtp-button-primary Update account settings \ No newline at end of file diff --git a/client/less/site/image.less b/client/less/site/image.less index 1faa6e5..509549e 100644 --- a/client/less/site/image.less +++ b/client/less/site/image.less @@ -25,7 +25,7 @@ img.site-profile-picture { height: auto; margin-left: auto; margin-right: auto; - border-radius: 50%; + border-radius: 5%; background-color: #8a8a8a; } diff --git a/config/limiter.js b/config/limiter.js index e45d925..350e855 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -146,6 +146,16 @@ module.exports = { expire: ONE_MINUTE, message: 'You are updating account settings too quickly', }, + getOtpSetup: { + total: 10, + expire: ONE_MINUTE, + message: 'You are configuring two-factor authentication too quickly', + }, + getOtpDisable: { + total: 10, + expire: ONE_MINUTE, + message: 'You are disabling two-factor authentication too quickly', + }, getSettings: { total: 8, expire: ONE_MINUTE,