diff --git a/packages/component-user-manager/src/Users.js b/packages/component-user-manager/src/Users.js
index 3a6774b2386056c681551de4afae99247d656845..d3a71ed9de7a52e50d913683cbf2027b70914a78 100644
--- a/packages/component-user-manager/src/Users.js
+++ b/packages/component-user-manager/src/Users.js
@@ -1,6 +1,6 @@
 const bodyParser = require('body-parser')
 
-const Invite = app => {
+const Users = app => {
   app.use(bodyParser.json())
   const authBearer = app.locals.passport.authenticate('bearer', {
     session: false,
@@ -125,4 +125,4 @@ const Invite = app => {
   )
 }
 
-module.exports = Invite
+module.exports = Users
diff --git a/packages/component-user-manager/src/routes/users/changePassword.js b/packages/component-user-manager/src/routes/users/changePassword.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1b49ae03fb012260a14a324a72640c1b4d48280
--- /dev/null
+++ b/packages/component-user-manager/src/routes/users/changePassword.js
@@ -0,0 +1,33 @@
+const { services } = require('pubsweet-component-helper-service')
+const { token } = require('pubsweet-server/src/authentication')
+
+module.exports = models => async (req, res) => {
+  const { password, newPassword } = req.body
+  if (!services.checkForUndefinedParams(password, newPassword))
+    return res.status(400).json({ error: 'Missing required params.' })
+
+  if (newPassword.length < 7)
+    return res
+      .status(400)
+      .json({ error: 'Password needs to be at least 7 characters long.' })
+
+  let user
+  try {
+    user = await models.User.find(req.user)
+    if (!user.validPassword(password)) {
+      return res.status(400).json({ error: 'Wrong username or password.' })
+    }
+    user.password = newPassword
+    user = await user.save()
+
+    return res.status(200).json({
+      ...user,
+      token: token.create(user),
+    })
+  } catch (e) {
+    const notFoundError = await services.handleNotFoundError(e, 'User')
+    return res.status(notFoundError.status).json({
+      error: notFoundError.message,
+    })
+  }
+}
diff --git a/packages/component-user-manager/src/tests/users/changePassword.test.js b/packages/component-user-manager/src/tests/users/changePassword.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..693dc6563d6c4dd7eab1a4b77cdc402531087e57
--- /dev/null
+++ b/packages/component-user-manager/src/tests/users/changePassword.test.js
@@ -0,0 +1,96 @@
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
+process.env.SUPPRESS_NO_CONFIG_WARNING = true
+
+const httpMocks = require('node-mocks-http')
+const cloneDeep = require('lodash/cloneDeep')
+const fixturesService = require('pubsweet-component-fixture-service')
+
+const { Model, fixtures } = fixturesService
+
+const { user } = fixtures.users
+jest.mock('pubsweet-component-mail-service', () => ({
+  sendSimpleEmail: jest.fn(),
+  sendNotificationEmail: jest.fn(),
+}))
+
+const reqBody = {
+  password: 'password',
+  newPassword: 'newPassword',
+}
+
+const notFoundError = new Error()
+notFoundError.name = 'NotFoundError'
+notFoundError.status = 404
+const changePasswordPath = '../../routes/users/changePassword'
+
+describe('Users password reset route handler', () => {
+  let testFixtures = {}
+  let body = {}
+  let models
+  beforeEach(() => {
+    testFixtures = cloneDeep(fixtures)
+    body = cloneDeep(reqBody)
+    models = Model.build(testFixtures)
+  })
+  it('should return an error when some parameters are missing', async () => {
+    delete body.password
+    const req = httpMocks.createRequest({ body })
+    req.user = user.id
+
+    const res = httpMocks.createResponse()
+    await require(changePasswordPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Missing required params.')
+  })
+  it('should return an error when the password is too small', async () => {
+    body.newPassword = 'small'
+    const req = httpMocks.createRequest({ body })
+    req.user = user.id
+
+    const res = httpMocks.createResponse()
+    await require(changePasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual(
+      'Password needs to be at least 7 characters long.',
+    )
+  })
+  it('should return an error when user is not found', async () => {
+    const req = httpMocks.createRequest({ body })
+    req.user = 'invalid-user-id'
+
+    const res = httpMocks.createResponse()
+    await require(changePasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(404)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('User not found')
+  })
+  it('should return an error when the current password is incorrect', async () => {
+    body.password = 'invalid-password'
+    const req = httpMocks.createRequest({ body })
+    req.user = user.id
+
+    const res = httpMocks.createResponse()
+    await require(changePasswordPath)(models)(req, res)
+    expect(res.statusCode).toBe(400)
+    const data = JSON.parse(res._getData())
+    expect(data.error).toEqual('Wrong username or password.')
+  })
+  it('should return success when the body is correct', async () => {
+    const req = httpMocks.createRequest({ body })
+    req.user = user.id
+
+    const res = httpMocks.createResponse()
+    await require(changePasswordPath)(models)(req, res)
+
+    expect(res.statusCode).toBe(200)
+    const savedUser = JSON.parse(res._getData())
+
+    savedUser.validPassword = user.validPassword
+    expect(savedUser.token).not.toEqual(user.token)
+
+    expect(savedUser.validPassword(body.newPassword)).toBeTruthy()
+  })
+})