-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: master
Are you sure you want to change the base?
Changes from all commits
faa81af
d3f3c6f
017eabe
1695c97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 protected] | ||
EMAIL_TRANSPORT=ses | ||
|
@@ -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 | ||
|
||
|
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' |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 || | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use the |
||
} | ||
|
||
return database.findUserByEmail(email) | ||
.then(user => { | ||
// If the user does not exist, use the check function anyways | ||
|
@@ -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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, we should use the |
||
console.log(error) | ||
return Promise.reject(error) | ||
} | ||
|
||
const { provider } = params | ||
delete params.provider | ||
if (!params.image) delete params.image // removes empty strings | ||
|
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) | ||
}) | ||
}) |
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) | ||
}) | ||
}) |
There was a problem hiding this comment.
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.