From faa81afb1876895596c4734a1d0f27951f5f82ed Mon Sep 17 00:00:00 2001 From: jordandenison Date: Mon, 21 Aug 2017 15:28:07 -0500 Subject: [PATCH 1/4] add --fix to lint command and ignore static directory --- package.json | 7 ++++++- src/services/users.js | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4773f21..a27e553 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test": "DATABASE_URL=postgresql://localhost/pnp PORT=3001 ava --verbose --fail-fast", "test:watch": "DATABASE_URL=postgresql://localhost/pnp PORT=3001 ava --verbose --fail-fast --watch", "test:dev": "NODE_ENV=test DATABASE_URL=postgresql://authtest:authtest@localhost/auth AUTH_SIGNUP_FIELDS=firstName,lastName,username AUTH_REDIRECT_URL=/auth/landing AUTH_BASE_URL=http://localhost:3000/auth SYMMETRIC_KEY=ee3b03dd1808d4172ee98ae6557c673c PORT=3001 ava --verbose", - "lint": "standard" + "lint": "standard --fix" }, "betterScripts": { "start": "node src/index.js", @@ -54,5 +54,10 @@ "chai": "4.1.1", "nodemon": "^1.11.0", "standard": "^8.6.0" + }, + "standard": { + "ignore": [ + "static/**" + ] } } diff --git a/src/services/users.js b/src/services/users.js index 20a0932..ae8ee9f 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -5,7 +5,7 @@ const _ = require('lodash') // We need to create this invalid hash, with passwords.hash, to prevent timing attacks (see below) let invalidHash = null passwords.hash('invalidEmail', 'anypasswordyoucanimagine') - .then(hash => (invalidHash = hash)) + .then(hash => (invalidHash = hash)) .catch(err => console.error(err)) const NUMBER_OF_RECOVERY_CODES = 10 From d3f3c6fa52df201aafabc05c7a0b9b5a80383fc7 Mon Sep 17 00:00:00 2001 From: jordandenison Date: Tue, 22 Aug 2017 11:07:00 -0500 Subject: [PATCH 2/4] implement white listed domains --- README.md | 4 +- docker-compose.yml | 36 +++++ src/services/users.js | 24 ++++ .../e2e.multiple-domains.js | 134 ++++++++++++++++++ test/whitelisted-domains/e2e.single-domain.js | 97 +++++++++++++ 5 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml create mode 100644 test/whitelisted-domains/e2e.multiple-domains.js create mode 100644 test/whitelisted-domains/e2e.single-domain.js diff --git a/README.md b/README.md index d1b3dd2..7e1e9dd 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ This is the list of available configuration options: | `TWILIO_ACCOUNT_SID` | Optional. Configure this for adding SMS support for 2FA | | `TWILIO_AUTH_TOKEN` | Optional. Configure this for adding SMS support for 2FA | | `TWILIO_NUMBER_FROM` | Optional. Configure this for adding SMS support for 2FA | +| `WHITELISTED_DOMAINS` | Optional. Limits creating/authenticating users on these specified domains only | The simplest JWT configuration is just setting up the `JWT_SECRET` value. @@ -140,6 +141,7 @@ AUTH_REDIRECT_URL=http://yourserver/callback AUTH_EMAIL_CONFIRMATION=true AUTH_STYLESHEET=http://yourserver/stylesheet.css JWT_SECRET=shhhh +WHITELISTED_DOMAINS=clevertech.biz,clevertech.com EMAIL_DEFAULT_FROM=hello@yourserver.com EMAIL_TRANSPORT=ses @@ -176,7 +178,7 @@ jwt.sign({ userId: user.id }) ## Security -This microservice is intended to be very secure. +This microservice is intented to be very secure. User accounts can be limited to certain domains by configuring the `WHITELISTED_DOMAINS` env variable. ### Forgot password functionality diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2eb044c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: '2' +services: + app: + build: . + command: yarn run start-dev + depends_on: + - mongo + - mysql + - postgres + environment: + JWT_SECRET: thebiggestsecretinclevertech + NODE_ENV: test + DATABASE_URL: postgresql://authtest:authtest@localhost/auth + AUTH_SIGNUP_FIELDS: firstName,lastName,username + AUTH_REDIRECT_URL: /auth/landing + AUTH_BASE_URL: http://localhost:3000/auth + SYMMETRIC_KEY: ee3b03dd1808d4172ee98ae6557c673c + PORT: 3001 + ports: + - '3000:3000' + - '3001:3001' + volumes: + - .:/opt/app + - /opt/app/node_modules + mongo: + image: mongo:3.4.4 + ports: + - '27018:27017' + mysql: + image: mysql:5.6.37 + ports: + - '3306:3306' + postgres: + image: postgres:9.6.4 + ports: + - '5432:5432' diff --git a/src/services/users.js b/src/services/users.js index ae8ee9f..09e6355 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -12,6 +12,8 @@ const NUMBER_OF_RECOVERY_CODES = 10 const normalizeEmail = email => email.toLowerCase() +const getEmailDomain = email => email.replace(/.*@/, '') + const userName = user => { return user.name || user.firstName || @@ -35,9 +37,24 @@ module.exports = (env, jwt, database, sendEmail, mediaClient, validations) => { return jwt.sign({ code: random() }, { expiresIn: '24h' }) } + let whiteListedDomains + try { + whiteListedDomains = env('WHITELISTED_DOMAINS').split(',').map(str => str.toLowerCase().trim()) + } catch (e) { + console.log(`WHITELISTED_DOMAINS not set as comma delimited string of email addresses properly, error: ${e.stack}`) + } + + const isDomainAuthorized = domain => !whiteListedDomains.length || whiteListedDomains.includes(domain) + return { login (email, password, client) { email = normalizeEmail(email) + if (!isDomainAuthorized(getEmailDomain(email))) { + const error = `non-whitelisted user "${email}" attempted to login` + console.log(error) + return Promise.reject(error) + } + return database.findUserByEmail(email) .then(user => { // If the user does not exist, use the check function anyways @@ -58,6 +75,13 @@ module.exports = (env, jwt, database, sendEmail, mediaClient, validations) => { }, register (params, client) { const email = normalizeEmail(params.email) + + if (!isDomainAuthorized(getEmailDomain(email))) { + const error = `non-whitelisted user "${email}" attempted to register` + console.log(error) + return Promise.reject(error) + } + const { provider } = params delete params.provider if (!params.image) delete params.image // removes empty strings diff --git a/test/whitelisted-domains/e2e.multiple-domains.js b/test/whitelisted-domains/e2e.multiple-domains.js new file mode 100644 index 0000000..5cf4aeb --- /dev/null +++ b/test/whitelisted-domains/e2e.multiple-domains.js @@ -0,0 +1,134 @@ +const test = require('ava') +const superagent = require('superagent') +const baseUrl = `http://127.0.0.1:3002` +const jwt = require('jsonwebtoken') + +const settings = { + JWT_ALGORITHM: 'HS256', + JWT_SECRET: 'shhhh', + MICROSERVICE_PORT: 3003, + WHITELISTED_DOMAINS: 'clevertech.biz,clevertech.com' +} + +const env = require('../src/utils/env')(settings) +const db = require('../src/database/adapter')(env) + +require('../').startServer(settings) // starts the app server + +// Declare some variables for storing things between tests +let _jwtToken +// Random number so that we don't have unique key collisions +const r = Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER) + +test('GET /auth/register', t => { + t.plan(2) + return superagent.get(`${baseUrl}/auth/register`) + .then((response) => { + t.is(response.statusCode, 200) + t.truthy(response.text.indexOf('= 0) + }) + .catch((error) => { + t.falsy(error) + }) +}) + +// Create a new account with first domain +test.serial('POST /auth/register', t => { + return superagent.post(`${baseUrl}/auth/register`) + .send(`firstName=Ian`) + .send(`lastName=McDevitt`) + .send(`username=ian`) + .send(`email=test%2B${r}@clevertech.biz`) + .send(`password=thisistechnicallyapassword`) + .then((response) => { + t.truthy(response.text.indexOf('

Before signing in, please confirm your email address.') >= 0) + }) + .catch((error) => { + t.falsy(error) + }) +}) + +// Create a new account with second domain +test.serial('POST /auth/register', t => { + return superagent.post(`${baseUrl}/auth/register`) + .send(`firstName=Ian`) + .send(`lastName=McDevitt`) + .send(`username=ian`) + .send(`email=test%2B${r}@clevertech.com`) + .send(`password=thisistechnicallyapassword`) + .then((response) => { + t.truthy(response.text.indexOf('

Before signing in, please confirm your email address.') >= 0) + }) + .catch((error) => { + t.falsy(error) + }) +}) + +// Mark that account as having a confirmed email address +// Then sign into that account with first whitelisted domain +test.serial('POST /auth/signin', t => { + return db.findUserByEmail(`test+${r}@clevertech.biz`) + .then(user => { + user.emailConfirmed = true + return db.updateUser(user) + .then((success) => { + t.truthy(success) + return superagent.post(`${baseUrl}/auth/signin`) + .send(`email=test%2B${r}@clevertech.biz`) + .send('password=thisistechnicallyapassword') + .then((response) => { + // Store the JWT for later use + _jwtToken = response.body + // Confirm that the JWT does indeed contain the data we want + const decoded = jwt.decode(_jwtToken) + t.is(decoded.user.email, `test+${r}@clevertech.biz`) + }) + .catch((error) => { + t.falsy(error) + }) + }) + }) +}) + +// Mark that account as having a confirmed email address +// Then sign into that account with second whitelisted domain +test.serial('POST /auth/signin', t => { + return db.findUserByEmail(`test+${r}@clevertech.com`) + .then(user => { + user.emailConfirmed = true + return db.updateUser(user) + .then((success) => { + t.truthy(success) + return superagent.post(`${baseUrl}/auth/signin`) + .send(`email=test%2B${r}@clevertech.com`) + .send('password=thisistechnicallyapassword') + .then((response) => { + // Store the JWT for later use + _jwtToken = response.body + // Confirm that the JWT does indeed contain the data we want + const decoded = jwt.decode(_jwtToken) + t.is(decoded.user.email, `test+${r}@clevertech.com`) + }) + .catch((error) => { + t.falsy(error) + }) + }) + }) +}) + +// Test not being able to sign into an account with a non valid domain +test.serial('POST /auth/signin', t => { + return superagent.post(`${baseUrl}/auth/signin`) + .send(`email=test%2B${r}@notclevertech.biz`) + .send('password=thisistechnicallyapassword') + .then((response) => { + // Store the JWT for later use + _jwtToken = response.body + // Confirm that the JWT does indeed contain the data we want + const decoded = jwt.decode(_jwtToken) + t.is(decoded.user.email, `test+${r}@clevertech.biz`) + }) + .catch((error) => { + t.falsy(error) + }) +}) diff --git a/test/whitelisted-domains/e2e.single-domain.js b/test/whitelisted-domains/e2e.single-domain.js new file mode 100644 index 0000000..826c9b7 --- /dev/null +++ b/test/whitelisted-domains/e2e.single-domain.js @@ -0,0 +1,97 @@ +const test = require('ava') +const superagent = require('superagent') +const baseUrl = `http://127.0.0.1:3001` +const jwt = require('jsonwebtoken') + +const settings = { + JWT_ALGORITHM: 'HS256', + JWT_SECRET: 'shhhh', + MICROSERVICE_PORT: 3002, + WHITELISTED_DOMAINS: 'clevertech.biz' +} + +const env = require('../src/utils/env')(settings) +const db = require('../src/database/adapter')(env) + +require('../').startServer(settings) // starts the app server + +// Declare some variables for storing things between tests +let _jwtToken +// Random number so that we don't have unique key collisions +const r = Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER) + +// Create a new account with a white-listed domain +test.serial('POST /auth/register', t => { + return superagent.post(`${baseUrl}/auth/register`) + .send(`firstName=Ian`) + .send(`lastName=McDevitt`) + .send(`username=ian`) + .send(`email=test%2B${r}@clevertech.biz`) + .send(`password=thisistechnicallyapassword`) + .then((response) => { + t.truthy(response.text.indexOf('

Before signing in, please confirm your email address.') >= 0) + }) + .catch((error) => { + t.falsy(error) + }) +}) + +// Don't create a new account with a non white-listed domain +test.serial('POST /auth/register', t => { + return superagent.post(`${baseUrl}/auth/register`) + .send(`firstName=Ian`) + .send(`lastName=McDevitt`) + .send(`username=ian`) + .send(`email=test%2B${r}@notclevertech.biz`) + .send(`password=thisistechnicallyapassword`) + .then((response) => { + // test error response somehow + t.truthy(true) + }) + .catch((error) => { + t.falsy(error) + }) +}) + +// Mark that account as having a confirmed email address +// Then sign into that account with a whitelisted domain +test.serial('POST /auth/signin', t => { + return db.findUserByEmail(`test+${r}@clevertech.biz`) + .then(user => { + user.emailConfirmed = true + return db.updateUser(user) + .then((success) => { + t.truthy(success) + return superagent.post(`${baseUrl}/auth/signin`) + .send(`email=test%2B${r}@clevertech.biz`) + .send('password=thisistechnicallyapassword') + .then((response) => { + // Store the JWT for later use + _jwtToken = response.body + // Confirm that the JWT does indeed contain the data we want + const decoded = jwt.decode(_jwtToken) + t.is(decoded.user.email, `test+${r}@clevertech.biz`) + }) + .catch((error) => { + t.falsy(error) + }) + }) + }) +}) + +// Test not being able to sign into an account with a non valid domain +test.serial('POST /auth/signin', t => { + return superagent.post(`${baseUrl}/auth/signin`) + .send(`email=test%2B${r}@notclevertech.biz`) + .send('password=thisistechnicallyapassword') + .then((response) => { + // Store the JWT for later use + _jwtToken = response.body + // Confirm that the JWT does indeed contain the data we want + const decoded = jwt.decode(_jwtToken) + t.is(decoded.user.email, `test+${r}@clevertech.biz`) + }) + .catch((error) => { + t.falsy(error) + }) +}) From 017eabe40b494ffd911ad6c77e867ce119b87afd Mon Sep 17 00:00:00 2001 From: jordandenison Date: Tue, 22 Aug 2017 13:46:28 -0500 Subject: [PATCH 3/4] fix intended typo in readme, switch console.log to console.error in users service and add comment to isDomainAuthorized function --- README.md | 2 +- src/services/users.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e1e9dd..d7351c3 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ jwt.sign({ userId: user.id }) ## Security -This microservice is intented to be very secure. User accounts can be limited to certain domains by configuring the `WHITELISTED_DOMAINS` env variable. +This microservice is intended to be very secure. User accounts can be limited to certain domains by configuring the `WHITELISTED_DOMAINS` env variable. ### Forgot password functionality diff --git a/src/services/users.js b/src/services/users.js index 09e6355..9c1f8f9 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -41,9 +41,10 @@ module.exports = (env, jwt, database, sendEmail, mediaClient, validations) => { try { whiteListedDomains = env('WHITELISTED_DOMAINS').split(',').map(str => str.toLowerCase().trim()) } catch (e) { - console.log(`WHITELISTED_DOMAINS not set as comma delimited string of email addresses properly, error: ${e.stack}`) + console.error(`WHITELISTED_DOMAINS not set as comma delimited string of email addresses properly, error: ${e.stack}`) } + // Allow all domains when no domains have been selected or check to see if the domain is one that has been whitelisted const isDomainAuthorized = domain => !whiteListedDomains.length || whiteListedDomains.includes(domain) return { From 1695c97801dfe90a3931f6e75e5cb841c462beb5 Mon Sep 17 00:00:00 2001 From: jordandenison Date: Tue, 22 Aug 2017 13:48:05 -0500 Subject: [PATCH 4/4] make sure whiteListedDomains is always an array in users service --- src/services/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/users.js b/src/services/users.js index 9c1f8f9..b717741 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -37,7 +37,7 @@ module.exports = (env, jwt, database, sendEmail, mediaClient, validations) => { return jwt.sign({ code: random() }, { expiresIn: '24h' }) } - let whiteListedDomains + let whiteListedDomains = [] try { whiteListedDomains = env('WHITELISTED_DOMAINS').split(',').map(str => str.toLowerCase().trim()) } catch (e) {