add length and character limitations for login name (username) (#9895)

* update API comments to for `username` restrictions and to use Login Name terminology

We use "login name" rather than "username" in user-visible text
on the website and (usually) when communicating with users because
"username" could be confused with "profile name".
Using it in the docs allows you to search for that term.

* add alphanumeric and length validation for creating new login name (username)

The 'en-US' locale is specified explicitly to ensure we never use
another locale. The point of this change is to limit the character
set to prevent login names being used to send spam in the Welcome
emails, such as Chinese language spam we've had trouble with.

* add error messages for bad login names

* allow login name to also contain hyphens

This is because our automated tests generate user accounts using:
  let username = generateUUID();

* allow login names to be up to 36 characters long because we use UUIDs as login names in our tests

* revert back to using max 20 characters and only a-z, 0-9 for login name.

It's been decided to change the username generation in the tests instead.

* disable test that is failing because it's redundant

Spaces are now prohibited by other code.

We can probably delete this test later. I don't want to delete it
now, but instead give us time to think about that.

* fix typos

* revert to login name restrictions that allow us to keep using our existing test code

I'm really not comfortable changing our test suite in ways that
aren't essential, especially since we're working in a hurry with
a larger chance than normal of breaking things.
The 36 character length is larger than we initially decided but
not so much larger that it's a huge problem.
We can reduce it to 20 when we have more time.

* limit username length to 20 chars

* fix tests
This commit is contained in:
Alys
2018-01-28 02:33:56 +10:00
committed by Keith Holliday
parent 8c70c8839b
commit f302d15bc4
5 changed files with 24 additions and 7 deletions

View File

@@ -149,7 +149,10 @@ describe('GET /challenges/:challengeId/members', () => {
let usersToGenerate = []; let usersToGenerate = [];
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
usersToGenerate.push(generateUser({challenges: [challenge._id]})); usersToGenerate.push(generateUser({
challenges: [challenge._id],
'profile.name': `${i}profilename`,
}));
} }
let generatedUsers = await Promise.all(usersToGenerate); let generatedUsers = await Promise.all(usersToGenerate);
let profileNames = generatedUsers.map(generatedUser => generatedUser.profile.name); let profileNames = generatedUsers.map(generatedUser => generatedUser.profile.name);

View File

@@ -6,10 +6,14 @@ import {
getProperty, getProperty,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes'; import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { v4 as generateRandomUserName } from 'uuid'; import { v4 as uuid } from 'uuid';
import { each } from 'lodash'; import { each } from 'lodash';
import { encrypt } from '../../../../../../website/server/libs/encryption'; import { encrypt } from '../../../../../../website/server/libs/encryption';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
}
describe('POST /user/auth/local/register', () => { describe('POST /user/auth/local/register', () => {
context('username and email are free', () => { context('username and email are free', () => {
let api; let api;
@@ -37,7 +41,8 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true); expect(user.newUser).to.eql(true);
}); });
it('remove spaces from username', async () => { xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces '; let username = ' usernamewithspaces ';
let email = 'test@example.com'; let email = 'test@example.com';
let password = 'password'; let password = 'password';

View File

@@ -15,7 +15,7 @@ import * as Tasks from '../../../../website/server/models/task';
// , you can do so by passing in the full path as a string: // , you can do so by passing in the full path as a string:
// { 'items.eggs.Wolf': 10 } // { 'items.eggs.Wolf': 10 }
export async function generateUser (update = {}) { export async function generateUser (update = {}) {
let username = generateUUID(); let username = (Date.now() + generateUUID()).substring(0, 20);
let password = 'password'; let password = 'password';
let email = `${username}@example.com`; let email = `${username}@example.com`;

View File

@@ -275,6 +275,8 @@
"emailTaken": "Email address is already used in an account.", "emailTaken": "Email address is already used in an account.",
"newEmailRequired": "Missing new email address.", "newEmailRequired": "Missing new email address.",
"usernameTaken": "Login Name already taken.", "usernameTaken": "Login Name already taken.",
"usernameWrongLength": "Login Name must be between 1 and 20 characters long.",
"usernameBadCharacters": "Login Name must contain only letters a to z, numbers 0 to 9, hyphens, or underscores.",
"passwordConfirmationMatch": "Password confirmation doesn't match password.", "passwordConfirmationMatch": "Password confirmation doesn't match password.",
"invalidLoginCredentials": "Incorrect username and/or email and/or password.", "invalidLoginCredentials": "Incorrect username and/or email and/or password.",
"passwordResetPage": "Reset Password", "passwordResetPage": "Reset Password",

View File

@@ -25,6 +25,8 @@ import { validatePasswordResetCodeAndFindUser, convertToBcrypt} from '../../libs
const BASE_URL = nconf.get('BASE_URL'); const BASE_URL = nconf.get('BASE_URL');
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL'); const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'); const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
const USERNAME_LENGTH_MIN = 1;
const USERNAME_LENGTH_MAX = 20;
let api = {}; let api = {};
@@ -78,11 +80,11 @@ function hasBackupAuth (user, networkToRemove) {
/** /**
* @api {post} /api/v3/user/auth/local/register Register * @api {post} /api/v3/user/auth/local/register Register
* @apiDescription Register a new user with email, username and password or attach local auth to a social user * @apiDescription Register a new user with email, login name, and password or attach local auth to a social user
* @apiName UserRegisterLocal * @apiName UserRegisterLocal
* @apiGroup User * @apiGroup User
* *
* @apiParam (Body) {String} username Username of the new user * @apiParam (Body) {String} username Login name of the new user. Must be 1-36 characters, containing only a-z, 0-9, hyphens (-), or underscores (_).
* @apiParam (Body) {String} email Email address of the new user * @apiParam (Body) {String} email Email address of the new user
* @apiParam (Body) {String} password Password for the new user * @apiParam (Body) {String} password Password for the new user
* @apiParam (Body) {String} confirmPassword Password confirmation * @apiParam (Body) {String} confirmPassword Password confirmation
@@ -101,7 +103,12 @@ api.registerLocal = {
notEmpty: {errorMessage: res.t('missingEmail')}, notEmpty: {errorMessage: res.t('missingEmail')},
isEmail: {errorMessage: res.t('notAnEmail')}, isEmail: {errorMessage: res.t('notAnEmail')},
}, },
username: {notEmpty: {errorMessage: res.t('missingUsername')}}, username: {
notEmpty: {errorMessage: res.t('missingUsername')},
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
// TODO use the constants in the error message above
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
},
password: { password: {
notEmpty: {errorMessage: res.t('missingPassword')}, notEmpty: {errorMessage: res.t('missingPassword')},
equals: {options: [req.body.confirmPassword], errorMessage: res.t('passwordConfirmationMatch')}, equals: {options: [req.body.confirmPassword], errorMessage: res.t('passwordConfirmationMatch')},