Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/16 limit domains #21

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 protected]
EMAIL_TRANSPORT=ses
Expand Down Expand Up @@ -176,7 +178,7 @@ jwt.sign({ userId: user.id })

## Security

This microservice is intended to be very secure.
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

Expand Down
36 changes: 36 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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'
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -54,5 +54,10 @@
"chai": "4.1.1",
"nodemon": "^1.11.0",
"standard": "^8.6.0"
},
"standard": {
"ignore": [
"static/**"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I should've done this but neglected to.

]
}
}
27 changes: 26 additions & 1 deletion src/services/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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

const normalizeEmail = email => email.toLowerCase()

const getEmailDomain = email => email.replace(/.*@/, '')

const userName = user => {
return user.name ||
user.firstName ||
Expand All @@ -35,9 +37,25 @@ 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.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 {
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the reject method from line 25? It has to be used with a string like this: reject('INVALID_CREDENTIALS') and then you will find a key in the i18n file for the message.

}

return database.findUserByEmail(email)
.then(user => {
// If the user does not exist, use the check function anyways
Expand All @@ -58,6 +76,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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we should use the reject method from line 25.

console.log(error)
return Promise.reject(error)
}

const { provider } = params
delete params.provider
if (!params.image) delete params.image // removes empty strings
Expand Down
134 changes: 134 additions & 0 deletions test/whitelisted-domains/e2e.multiple-domains.js
Original file line number Diff line number Diff line change
@@ -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('<html') >= 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('<p>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('<p>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)
})
})
97 changes: 97 additions & 0 deletions test/whitelisted-domains/e2e.single-domain.js
Original file line number Diff line number Diff line change
@@ -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('<p>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)
})
})