Kaynağa Gözat

Merge pull request #15728 from overleaf/td-lg-expire-password-tokens

Expire user password reset tokens when user changes their password

GitOrigin-RevId: 2d303eba947c224e71ebe60083abc7a8ff5207a5
Tim Down 2 yıl önce
ebeveyn
işleme
2807a35e24

+ 19 - 0
services/web/app/src/Features/Security/OneTimeTokenHandler.js

@@ -112,6 +112,25 @@ const OneTimeTokenHandler = {
       }
     )
   },
+
+  expireAllTokensForUser(userId, use, callback) {
+    const now = new Date()
+    db.tokens.updateMany(
+      {
+        use,
+        'data.user_id': userId.toString(),
+        usedAt: { $exists: false },
+      },
+      {
+        $set: {
+          usedAt: now,
+        },
+      },
+      error => {
+        callback(error)
+      }
+    )
+  },
 }
 
 OneTimeTokenHandler.promises = promisifyAll(OneTimeTokenHandler)

+ 6 - 0
services/web/app/src/Features/User/UserController.js

@@ -22,6 +22,7 @@ const {
   acceptsJson,
 } = require('../../infrastructure/RequestContentTypeDetection')
 const Modules = require('../../infrastructure/Modules')
+const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler')
 
 async function _sendSecurityAlertClearedSessions(user) {
   const emailOptions = {
@@ -131,6 +132,11 @@ async function changePassword(req, res, next) {
     req.sessionID,
   ])
 
+  await OneTimeTokenHandler.promises.expireAllTokensForUser(
+    userId.toString(),
+    'password'
+  )
+
   return res.json({
     message: {
       type: 'success',

+ 22 - 0
services/web/migrations/20231113173237_tokens_user_id_index.js

@@ -0,0 +1,22 @@
+/* eslint-disable no-unused-vars */
+
+const Helpers = require('./lib/helpers')
+
+exports.tags = ['server-ce', 'server-pro', 'saas']
+
+const index = {
+  key: {
+    'data.user_id': 1,
+  },
+  name: 'data.user_id_1',
+}
+
+exports.migrate = async client => {
+  const { db } = client
+  await Helpers.addIndexesToCollection(db.tokens, [index])
+}
+
+exports.rollback = async client => {
+  const { db } = client
+  await Helpers.dropIndexesFromCollection(db.tokens, [index])
+}

+ 16 - 0
services/web/test/unit/src/User/UserControllerTests.js

@@ -129,6 +129,10 @@ describe('UserController', function () {
       promises: { sendEmail: sinon.stub().resolves() },
     }
 
+    this.OneTimeTokenHandler = {
+      promises: { expireAllTokensForUser: sinon.stub().resolves() },
+    }
+
     this.UserController = SandboxedModule.require(modulePath, {
       requires: {
         '../Helpers/UrlHelper': this.UrlHelper,
@@ -149,6 +153,7 @@ describe('UserController', function () {
         '@overleaf/settings': this.settings,
         '@overleaf/o-error': OError,
         '../Email/EmailHandler': this.EmailHandler,
+        '../Security/OneTimeTokenHandler': this.OneTimeTokenHandler,
         '../../infrastructure/RequestContentTypeDetection':
           this.RequestContentTypeDetection,
       },
@@ -739,6 +744,17 @@ describe('UserController', function () {
         })
         this.UserController.changePassword(this.req, this.res)
       })
+
+      it('should expire password reset tokens', function (done) {
+        this.res.json.callsFake(() => {
+          this.OneTimeTokenHandler.promises.expireAllTokensForUser.should.have.been.calledWith(
+            this.user._id,
+            'password'
+          )
+          done()
+        })
+        this.UserController.changePassword(this.req, this.res)
+      })
     })
 
     describe('errors', function () {