diff --git a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js index 6a29d56b49..a447bf68d7 100644 --- a/test/api/v3/integration/user/auth/POST-user_auth_social.test.js +++ b/test/api/v3/integration/user/auth/POST-user_auth_social.test.js @@ -65,6 +65,52 @@ describe('POST /user/auth/social', () => { await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user'); }); + it('includes sanitized version of provided username', async () => { + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'Google User Name', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.equal('GoogleUserName'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername'); + }); + + it('generates a random username if provided username contains only disallowed characters', async () => { + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'Áîüè', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); + }); + + it('generates a random username if provided username contains a disallowed word', async () => { + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'i am a TESTPLACEHOLDERSLURWORDHERE', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); + }); + + it('generates a random username if sanitized username conflicts with an extant user', async () => { + user = await generateUser({ 'auth.local.username': 'GoogleUserName' }); + + const response = await api.post(endpoint, { + authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase + network, + username: 'Google User Name', + }); + + await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-'); + await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-'); + }); + it('fails if allowRegister is false and user does not exist', async () => { await expect(api.post(endpoint, { authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase diff --git a/website/server/libs/auth/social.js b/website/server/libs/auth/social.js index a6101378d5..945d06f81a 100644 --- a/website/server/libs/auth/social.js +++ b/website/server/libs/auth/social.js @@ -1,6 +1,7 @@ import pick from 'lodash/pick'; import passport from 'passport'; import common from '../../../common'; +import { verifyUsername } from '../user/validation'; import { BadRequest, NotAuthorized, NotFound } from '../errors'; import logger from '../logger'; import { @@ -70,7 +71,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre if (!user.auth.local.email) { user.auth.local.email = await socialEmailToLocal(user); } - // Force the updated timestampt to update, so that we know they logged in + // Force the updated timestamp to save, so that we know they logged in user.auth.timestamps.updated = new Date(); await user.save(); return loginRes(user, req, res); @@ -82,6 +83,7 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre } if (!existingUser && email) { + // TODO we load the whole user object here. Is that necessary? existingUser = await User.findOne({ 'auth.local.email': email }).exec(); } @@ -98,6 +100,19 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre throw new NotFound(res.t('userNotFound')); } + let sanitizedUsername = username.replace(/[^a-zA-Z0-9_-]/g, ''); + const issues = verifyUsername(sanitizedUsername, res, true); + if (issues.length > 0) { + sanitizedUsername = generateUsername(); + } else { + const conflictingUser = await User.findOne({ + 'auth.local.lowerCaseUsername': sanitizedUsername.toLowerCase(), + }, { _id: 1 }); + if (conflictingUser) { + sanitizedUsername = generateUsername(); + } + } + if (existingUser) { existingUser.auth[network] = { id: profile.id, @@ -112,8 +127,8 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre emails: profile.emails, }, local: { - username, - lowerCaseUsername: username.toLowerCase(), + username: sanitizedUsername, + lowerCaseUsername: sanitizedUsername.toLowerCase(), email, }, },