Skip to content

Commit 63d8ffb

Browse files
authored
feat: optional password strength checks (#429)
1 parent a82eb9b commit 63d8ffb

27 files changed

+1146
-440
lines changed

.mdeprc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"node": "10.16.1",
2+
"node": "10.16.3",
33
"rebuild": ["ms-flakeless", "scrypt"]
44
}

doc/password_validator.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Password validator
2+
The validator provides a complexity check for AJV validator schemas when initialized defines `password` keyword.
3+
Validator dependencies:
4+
- `dropbox/zxcvbn` complexity checker
5+
- `mfleet.validator` plugin.
6+
7+
## Configuration
8+
Validator configuration stored under `service.config.passwordValidator` key:
9+
10+
### minStrength _int_
11+
Sets minimal password complexity to accept a given password.
12+
13+
### skipCheckFieldNames _string[]_
14+
Allows skipping field validation:
15+
16+
- When passed field value is `boolean`:
17+
* Validation skipped if `Object[field]` === `true`.
18+
* Validation performed if `Object[field]` === `false`.
19+
- When passed field value is `any` type except `boolean`:
20+
* Validation skipped if `Object[field]` set.
21+
* Validation performed if `Object[field]` not exists.
22+
23+
E.g.:
24+
```js
25+
const validatorConfig = {
26+
minStrength: 4,
27+
enabled: true,
28+
skipCheckFieldNames: ['skipPassword'],
29+
};
30+
31+
// Skip validation
32+
const dataSkipOnBool = {
33+
myField: 'fooBar', // schema "myField":{"password": true} keyword
34+
skipPassword: true,
35+
};
36+
37+
// Skip validation `skipPassword` is string
38+
const dataSkipOnString = {
39+
myField: 'fooBar',
40+
skipPassword: 'fooStringValue',
41+
};
42+
43+
// Validation performed
44+
const dataSkipOnBoolFalse = {
45+
myField: 'fooBar',
46+
skipPassword: false,
47+
};
48+
49+
const data = {
50+
myField: 'fooBar',
51+
skipPassword: true,
52+
};
53+
54+
```
55+
56+
### forceCheckFieldNames __string[]__
57+
Allows to force Validation event if the Validator is disabled:
58+
- When any of passed fields value is `boolean`:
59+
* Validation performed if `Object[field]` === `true`.
60+
* Validation skipped if `Object[field]` === `false`.
61+
- When any of passed fields value is `any` type except `boolean`:
62+
* Validation performed if `Object[field]` set.
63+
* Validation skipped if `Object[field]` not exists.
64+
65+
```js
66+
const validatorConfig = {
67+
minStrength: 4,
68+
enabled: false, // Will validate the `data` object anyway
69+
forceCheckFieldNames: ['forceValidate'],
70+
}
71+
72+
// Force validation
73+
const data = {
74+
myField: 'fooBar', // in schema "myField":{"password": true} keyword.
75+
forceValidate: true,
76+
};
77+
78+
// Force validation, `forceValidate` is a string.
79+
const dataSkipOnString = {
80+
myField: 'fooBar',
81+
forceValidate: 'fooStringValue',
82+
};
83+
84+
// Validation NOT performed.
85+
const dataSkipOnBoolFalse = {
86+
myField: 'fooBar',
87+
forceValidate: false,
88+
};
89+
90+
// Validation NOT performed.
91+
const data = {
92+
myField: 'fooBar',
93+
};
94+
```
95+
96+
### inputFieldNames _string[]_
97+
Allows using additional fields from the parent object. These values used inside `zxcvbn` to avoid sensitive information inside passwords.
98+
```js
99+
const data = {
100+
username: 'foouser',
101+
password: 'foouser1232',
102+
altname: 'barname',
103+
};
104+
105+
// if 'username' and 'password' or other data will match, the complexity level dropped.
106+
const validatorConfig = {
107+
minStrength: 4,
108+
inputFieldNames: [
109+
'username',
110+
'altname',
111+
]
112+
}
113+
```
114+
115+
## Schema
116+
Add the `password` keyword to any field in your schema.
117+
```json
118+
{
119+
"$id": "register",
120+
"type": "object",
121+
"required": [
122+
"username",
123+
"audience"
124+
],
125+
"properties": {
126+
"username": {
127+
"type": "string"
128+
},
129+
"alias": {
130+
"type": "string"
131+
},
132+
"password": {
133+
"type": "string",
134+
"password":true
135+
},
136+
"audience": {
137+
"type": "string",
138+
"minLength": 1
139+
}
140+
}
141+
}
142+
```

package.json

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,23 @@
2828
},
2929
"homepage": "https://github.com/makeomatic/ms-users#readme",
3030
"dependencies": {
31-
"@hapi/bell": "^10.1.1",
32-
"@hapi/hapi": "^18.3.2",
33-
"@hapi/vision": "^5.5.3",
34-
"@microfleet/core": "^14.0.3",
31+
"@hapi/bell": "^11.1.0",
32+
"@hapi/hapi": "^18.4.0",
33+
"@hapi/vision": "^5.5.4",
34+
"@microfleet/core": "^14.1.2",
3535
"@microfleet/transport-amqp": "^15.0.0",
3636
"@microfleet/validation": "^8.1.2",
3737
"bluebird": "^3.5.5",
3838
"bytes": "^3.0.0",
3939
"common-errors": "^1.0.5",
4040
"csv-write-stream": "^2.0.0",
4141
"disposable-email-domains": "^1.0.48",
42-
"dlock": "^9.0.1",
42+
"dlock": "^10.0.0",
4343
"flake-idgen": "^1.1.0",
4444
"get-stdin": "^7.0.0",
4545
"get-value": "^3.0.1",
46-
"handlebars": "^4.2.0",
47-
"ioredis": "^4.14.0",
46+
"handlebars": "^4.3.3",
47+
"ioredis": "^4.14.1",
4848
"is": "^3.3.0",
4949
"jsonwebtoken": "^8.5.1",
5050
"jwa": "^1.4.1",
@@ -58,51 +58,52 @@
5858
"otplib": "^11.0.1",
5959
"password-generator": "^2.2.0",
6060
"prom-client": "^11.5.3",
61-
"qs": "^6.8.0",
61+
"qs": "^6.9.0",
6262
"redis-filtered-sort": "^2.3.0",
6363
"request": "^2.88.0",
6464
"request-promise": "^4.2.4",
6565
"scrypt": "^6.0.1",
66-
"serialize-error": "^4.1.0",
66+
"serialize-error": "^5.0.0",
6767
"serialize-javascript": "^2.1.0",
6868
"stdout-stream": "^1.4.1",
6969
"tough-cookie": "^3.0.0",
7070
"uuid": "^3.3.3",
71-
"yargs": "^14.0.0"
71+
"yargs": "^14.0.0",
72+
"zxcvbn": "^4.4.2"
7273
},
7374
"devDependencies": {
74-
"@babel/cli": "^7.6.0",
75-
"@babel/core": "^7.6.0",
75+
"@babel/cli": "^7.6.2",
76+
"@babel/core": "^7.6.2",
7677
"@babel/plugin-proposal-class-properties": "^7.5.5",
77-
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
78+
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
7879
"@babel/plugin-transform-strict-mode": "^7.2.0",
79-
"@babel/register": "^7.6.0",
80-
"@makeomatic/deploy": "^8.4.7",
80+
"@babel/register": "^7.6.2",
81+
"@makeomatic/deploy": "^9.1.0",
8182
"@semantic-release/changelog": "^3.0.4",
82-
"@semantic-release/exec": "^3.3.6",
83+
"@semantic-release/exec": "^3.3.7",
8384
"@semantic-release/git": "^7.0.16",
8485
"apidoc": "^0.17.7",
8586
"apidoc-plugin-schema": "^0.1.8",
8687
"babel-eslint": "^10.0.3",
8788
"babel-plugin-istanbul": "^5.2.0",
8889
"chai": "^4.2.0",
8990
"cheerio": "^1.0.0-rc.3",
90-
"codecov": "^3.5.0",
91-
"cross-env": "^5.2.1",
92-
"eslint": "^6.3.0",
91+
"codecov": "^3.6.1",
92+
"cross-env": "^6.0.0",
93+
"eslint": "^6.4.0",
9394
"eslint-config-makeomatic": "^3.1.0",
9495
"eslint-plugin-import": "^2.18.2",
95-
"eslint-plugin-mocha": "^6.1.0",
96+
"eslint-plugin-mocha": "^6.1.1",
9697
"eslint-plugin-promise": "^4.2.1",
9798
"faker": "^4.1.0",
9899
"glob": "^7.1.4",
99100
"json": "^9.0.6",
100101
"md5": "^2.2.1",
101102
"mocha": "^6.2.0",
102103
"nyc": "^14.1.1",
103-
"puppeteer": "1.19.0",
104+
"puppeteer": "1.20.0",
104105
"rimraf": "^3.0.0",
105-
"sinon": "^7.4.2"
106+
"sinon": "^7.5.0"
106107
},
107108
"engines": {
108109
"node": ">= 10.10.0",

rfcs/password_validation_limits.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Password Validation
2+
3+
## Overview and motivation
4+
Currently, `ms-users` service allows any passwords in the user registration request. But this brings some user-side security vulnerabilities.
5+
6+
General policies describe that password should:
7+
- Minimum length starts from 8 chars
8+
- Contain different character cases
9+
- Contain some digits
10+
- Shouldn't have many repeating characters
11+
- Shouldn't be in common passwords list
12+
13+
Additional password validation should be added to provide better security for users.
14+
15+
## General
16+
A possible solution is to use existing password-strength checking solution like `dropbox/zxcvbn` - provides password strength check using predefined dictionaries and pattern matching. With password ~25 char length, projected latency is ~5-25ms. In the future, we can extend included dictionaries according to our needs.
17+
18+
## Note
19+
Enabling password validator is Breaking change and disabled by default.
20+
In the nearest future, when everything will be ready, a validator must be enabled.
21+
22+
## OTP and Empty Password
23+
The `password` field is not defined as `required` in current `schemas`.
24+
Validation is not performed because this field does not exist in the OTP registration request.
25+
When the Service performs password generation, `skipPassword` property does not exist in the registration request, so validator does not check these generated passwords.
26+
27+
**NOTE:** Added additional test suites just to be sure.
28+
29+
## Validator Keyword
30+
Service validation algorithm based on the AJV validator and all validation requirements are inside schemas. All incoming requests checked. We can add additional custom validator `password` keyword for the `password` field, which performs required checks.
31+
32+
Current schema:
33+
34+
```json
35+
{
36+
"$id": "register",
37+
"type": "object",
38+
"password": {
39+
"type": "string"
40+
}
41+
}
42+
```
43+
44+
After keyword added:
45+
```json
46+
{
47+
"$id": "register",
48+
"type": "object",
49+
"password": {
50+
"type": "string",
51+
"password": true
52+
}
53+
}
54+
```
55+
56+
## `password`
57+
The parameter accepts the required strength in the range [0-4] that describes the required password complexity.
58+
59+
According to the registration process, some passwords generated by service, so custom validator should omit password check if `skipPassword` request param provided.
60+
61+
When executed:
62+
- checks username does not match provided password
63+
- calls `zxcvbn` to obtain a complexity of passwords and returns whether the given password matches policy
64+
65+
Also, we can pass user-provided data into `zxcvbn` to check whether some sensitive data used in the password.
66+
67+
### Sample config
68+
`skipCheckFieldName` - Allows skipping password check if any field exists.
69+
`forceCheckFieldName` - Allows forcing password check if any field exists.
70+
`inputFieldNames` - values point to parent object fields that passed into `zxcvbn`.
71+
72+
`enabled` - disable/enable validator. Default value is false.
73+
```js
74+
const config = {
75+
enabled: false,
76+
minStrength: 3, // Desired strength
77+
skipCheckFieldNames: ['skipPassword'], // Disables password check if the object field value exists.
78+
forceCheckFieldNames: ['checkPassword'], // Forces password check if the object field value exists.
79+
inputFieldNames: [ // Linked fields list, allows to filter the sensitive data in the password from the parent object.
80+
'username',
81+
'otherfield'
82+
]
83+
}
84+
```

schemas/config.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"deleteInactiveAccounts",
88
"jwt",
99
"validation",
10+
"passwordValidator",
1011
"server",
1112
"mailer",
1213
"pwdReset",
@@ -276,6 +277,50 @@
276277
}
277278
}
278279
},
280+
"passwordValidator": {
281+
"type": "object",
282+
"required": [
283+
"enabled",
284+
"minStrength",
285+
"inputFieldNames"
286+
],
287+
"properties": {
288+
"enabled": {
289+
"type": "boolean",
290+
"default": "false"
291+
},
292+
"minStrength": {
293+
"type": "integer",
294+
"default" : 4,
295+
"minimum": 0,
296+
"maximum": 4
297+
},
298+
"forceCheckFieldNames": {
299+
"type": "array",
300+
"items": {
301+
"type": "string",
302+
"minLength": 1
303+
},
304+
"default": []
305+
},
306+
"skipCheckFieldNames": {
307+
"type": "array",
308+
"items": {
309+
"type": "string",
310+
"minLength": 1
311+
},
312+
"default": []
313+
},
314+
"inputFieldNames": {
315+
"type": "array",
316+
"items": {
317+
"type": "string",
318+
"minLength": 1
319+
},
320+
"default": []
321+
}
322+
}
323+
},
279324
"server": {
280325
"required": [
281326
"proto",

0 commit comments

Comments
 (0)