Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
10a9c7d
first work
julienw Dec 10, 2016
1b4a928
facebook strategy
julienw Dec 22, 2016
05d2eb3
Facebook flow works
julienw Dec 22, 2016
002a80c
store token in localStorage
julienw Dec 23, 2016
631688a
Better file naming
julienw Jan 3, 2017
92085bf
Add user in the DB
julienw Jan 6, 2017
0598b42
Configuration precisions
julienw Jan 9, 2017
f365472
minor fix
julienw Jan 9, 2017
e8520e7
makes user registration work
julienw Jan 9, 2017
4e48c20
unique key includes a client
julienw Jan 9, 2017
7eeb53d
force a client in the schema
julienw Jan 9, 2017
590ead3
some changes to get clients
julienw Jan 10, 2017
cb528f3
make things somewhat work
julienw Jan 12, 2017
cf40a02
bad basic auth is a proprt 401
julienw Jan 13, 2017
cfdb882
full facebook authentication works
julienw Jan 13, 2017
21daea9
Properly manage failure cases
julienw Jan 13, 2017
101b0d0
Implement "a secret for each client" feature
julienw Jan 13, 2017
84eef09
remove eslintrc
julienw Jan 13, 2017
79265de
fix tests
julienw Jan 17, 2017
ee83cb7
test change
julienw Jan 17, 2017
cfcc0ca
more test changes
julienw Jan 18, 2017
57bf038
Working facebook flow test
julienw Jan 18, 2017
cb52e46
Check a user has been created
julienw Jan 18, 2017
5f8f9fe
Run all tests
julienw Jan 18, 2017
706ab7b
Remove unused passport backends
julienw Jan 18, 2017
93db8e2
put facebook config in a separate part
julienw Jan 18, 2017
aa8fb3b
revert changes in config.js
julienw Jan 18, 2017
d732f30
simplifying a bit of code in the authentication logic
julienw Jan 18, 2017
a1a8b26
indentation, rename utils to finalizeAuth, rename authRoute, extract …
julienw Jan 18, 2017
7d31a1b
Proxy and cookie setup
julienw Jan 18, 2017
5400675
indentation and comments
julienw Jan 18, 2017
d4c32d5
ClientKey -> clientKey
julienw Jan 18, 2017
89e8dfc
move from req.user to req[scope]
julienw Jan 18, 2017
07ba7f7
add some logs
julienw Jan 18, 2017
df6d2a9
revert lint fix
julienw Jan 18, 2017
00649fa
circleci config
julienw Jan 18, 2017
2b93393
Last review comments
julienw Jan 19, 2017
c5a5f6b
Use only req.user during authentication
julienw Jan 19, 2017
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
7 changes: 7 additions & 0 deletions config/circleci.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@
"user": "ubuntu",
"password": ""
},
"userAuth": {
"sessionSecret": "v3erY secr€t",
"facebook": {
"clientID": "some_client_id",
"clientSecret": "some_client_secret"
}
},
"version": "v1.0"
}
8 changes: 8 additions & 0 deletions config/sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
"adminSessionSecret": "default",
"env": "dev",
"port": 8080,
"publicHost": "http://localhost:8080",
"db": {
"host": "localhost",
"port": 5432,
"name": "sensorweb",
"user": "postgres",
"password": "default"
},
"userAuth": {
"sessionSecret": "default",
"facebook": {
"clientID": "",
"clientSecret": ""
}
},
"sensorthings": {
"server": "https://pg-api.sensorup.com",
"path": "/st-playground/proxy/v1.0",
Expand Down
8 changes: 8 additions & 0 deletions config/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
"adminSessionSecret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"env": "test",
"port": 8080,
"publicHost": "http://localhost:8080",
"db": {
"host": "localhost",
"port": 5432,
"name": "sensorwebtest",
"user": "postgres",
"password": "default"
},
"userAuth": {
"sessionSecret": "v3erY secr€t",
"facebook": {
"clientID": "some_client_id",
"clientSecret": "some_client_secret"
}
},
"version": "v1.0"
}
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,36 @@
"babel-cli": "^6.18.0",
"babel-preset-env": "^1.1.8",
"babel-register": "^6.18.0",
"co": "^4.6.0",
"co-mocha": "^1.1.3",
"coveralls": "^2.11.14",
"istanbul": "^0.4.5",
"mocha": "^3.1.0",
"nock": "^9.0.2",
"nodemon": "^1.10.2",
"nyc": "^8.3.1",
"should": "^11.1.0",
"supertest": "^2.0.0"
"supertest": "^2.0.0",
"supertest-as-promised": "^4.0.2"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is nice as we can work with promises instead of using end + done callback.

Copy link
Member

Choose a reason for hiding this comment

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

Nice!

},
"dependencies": {
"body-parser": "^1.15.2",
"btoa": "^1.1.2",
"connect-session-sequelize": "^4.1.0",
"convict": "^1.5.0",
"cors": "^2.8.1",
"express": "^4.14.0",
"express-http-proxy": "^0.10.0",
"express-session": "^1.14.2",
"express-validator": "^2.20.10",
"jsonwebtoken": "^7.1.9",
"lodash": "^4.15.0",
"morgan-body": "^0.9.1",
"on-headers": "^1.0.1",
"owasp-password-strength-test": "^1.3.0",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-http": "^0.3.0",
"pg": "^6.1.0",
"sensorthings": "^0.0.10",
"sequelize": "^3.24.5"
Expand Down
45 changes: 45 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ convict.addFormat({
coerce: (val) => (val === null ? null : parseInt(val))
});

convict.addFormat({
name: 'hex',
validate: function(val) {
if (/[^a-fA-F0-9]/.test(val)) {
throw new Error('must be a hex key');
}
}
});

const conf = convict({
adminPass: {
doc: 'The password for the admin user. Follow OWASP guidelines for passwords',
Expand Down Expand Up @@ -72,6 +81,9 @@ const conf = convict({
default: '',
}
},
publicHost: {
doc: 'Public host for this server, especially for auth callback'
},
sensorthings: {
server: {
doc: 'SensorThings remote API server',
Expand All @@ -93,6 +105,39 @@ const conf = convict({
}
}
},
behindProxy: {
doc: `Set this to true if the server runs behind a reverse proxy. This is
especially important if the proxy implements HTTPS with
userAuth.cookieSecure. Also with this Express will trust the
X-Forwarded-For header. Set to 1 or auto if you're behind a proxy.`,
default: false,
// Format is "*" because otherwise convict infers it's a boolean from the
// default value and will refuse 1 or "auto".
format: '*',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

format is "*" because otherwise convict infers it's a boolean from the default and will refuse 1 or "auto".

Copy link
Member

Choose a reason for hiding this comment

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

Could you add all these comments as in code comments, please?

},
userAuth: {
cookieSecure: {
doc: `This configures whether the cookie should be set and sent for
HTTPS only. This is important to set to 'true' if the application
runs on HTTPS.`,
default: false,
},
sessionSecret: {
doc: 'This secret is used to sign session cookie',
default: defaultValue,
format: avoidDefault,
},
facebook: {
clientId: {
doc: 'Facebook clientId',
format: 'nat'
},
clientSecret: {
doc: 'Facebook clientSecret',
format: 'hex'
},
},
},
version: {
doc: 'API version. We follow SensorThing\'s versioning format as described at http://docs.opengeospatial.org/is/15-078r6/15-078r6.html#34',
format: value => {
Expand Down
76 changes: 48 additions & 28 deletions src/middlewares/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import jwt from 'jsonwebtoken';
import config from '../config';
import db from '../models/db';

import {
ApiError,
Expand All @@ -16,13 +17,25 @@ function unauthorized(res) {
}

export default (scopes) => {
// For now we only allow 'admin' scope.
const validScopes = ['admin', 'client', 'user'].filter(
scope => scopes.includes(scope)
);

if (!validScopes.length) {
throw new Error(`No valid scope found in "${scopes}"`);
}

return (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) {
const authHeader = req.headers.authorization;
// Accepting a query parameter as well allows GET requests.
const authQuery = req.query.authorizationToken;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To be able to use a simple browser request, we can pass the JWT token in this query parameter. I haven't looked if there is anything kinda standardized for this yet.


if (!authHeader && !authQuery) {
return unauthorized(res);
}

const token = authHeader.split('Bearer ')[1];
const token = authHeader ? authHeader.split('Bearer ')[1] : authQuery;
if (!token) {
return unauthorized(res);
}
Copy link
Member

Choose a reason for hiding this comment

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

Maybe:

const token = (authHeader && authHeader.split('Bearer ')[1]) || authQuery;
if (!token) {
  return unauthorized(res);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah why not; I can use a ternary which can be a tiny bit more readable.

Expand All @@ -31,36 +44,43 @@ export default (scopes) => {
// need to get the owner of the token so we can get the appropriate secret.
const decoded = jwt.decode(token);

// For now we only allow authenticated requests from the admin user.
// When this changes we will have a different secret per sensor and per
// user.
if (!decoded || !decoded.id || decoded.id !== 'admin') {
if (!decoded || !decoded.id || !decoded.scope) {
return unauthorized(res);
};
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

note: directly calling unauthorized disallows the redirect to failureUrl; this will be fixed once we just pass an error to next() so that we can handle it (or not) in the next middleware.

const secret = config.get('adminSessionSecret');
if (!validScopes.includes(decoded.scope)) {
console.log('Error while authenticating, invalid scope', decoded);
return unauthorized(res);
}

// Verify JWT signature.
jwt.verify(token, secret, (error, decoded) => {
if (error) {
return unauthorized(res);
}
let secretPromise;
switch(decoded.scope) {
case 'client':
secretPromise = db().then(({ Clients }) =>
Clients.findById(decoded.id, { attributes: ['secret'] })
).then(client => client.secret);
break;
case 'user':
case 'admin':
secretPromise = Promise.resolve(config.get('adminSessionSecret'));
break;
default:
// should not happen because we check this earlier
next(new Error(`Unknown scope ${decoded.scope}`));
}

// XXX Get allowed scopes from sensor/user.
// Verify JWT signature.
secretPromise.then(secret => {
jwt.verify(token, secret, (error) => {
if (error) {
console.log('Error while verifying the token', error);
return unauthorized(res);
}

// For now we only allow 'admin' scope.
const validScopes = ['admin'].filter(scopeIndex => {
return scopes.indexOf(scopeIndex) != -1;
req[decoded.scope] = decoded.id;
req.authScope = decoded.scope;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

these 2 lines are repeated a few times, it's likely a good idea to factorize this, but maybe in a separate patch ?

Copy link
Member

Choose a reason for hiding this comment

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

Probably, but where are the repetitions? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

there are 3 locations where we set req[XXX.scope] and req.authScope: here, and in all auth/<method>.js (basic, facebook, and later google and fxaccounts likely).

That said, maybe we don't want this in auth/<method>.js where it's only consumed by finalizeAuth; we could simply use req.user or even something else like req.auth.

I'll think of this and maybe we can rework it when implementing google and fx accounts.

Copy link
Member

Choose a reason for hiding this comment

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

File another issue and reference it in the code, please :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

after all I just did what I suggested in a separate commit. Easier than creating an issue :) Tell me what you think.

return next();
});

if (!validScopes.length) {
return unauthorized(res);
}

// If everything is good, save the decoded payload for use in other
// routes.
req.decoded = decoded;
next();
});
}).catch(err => next(err || new Error('Unexpected error')));
};
};
32 changes: 16 additions & 16 deletions src/models/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ let deferreds = [];
let state = IDLE;
let db = null;

module.exports = () => {
const { name, user, password, host, port } = config.get('db');

const sequelize = new Sequelize(name, user, password, {
host,
port,
dialect: 'postgres',
logging: false
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

extracting this out of the exported function makes it possible to export it so that we can use it elsewhere. (in my case: for the session store)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think this makes a big perf difference, as I believe what makes the big perf hit is loading all the model and calling sync


export default function() {
if (state === READY) {
return Promise.resolve(db);
}
Expand All @@ -41,24 +50,13 @@ module.exports = () => {

state = INITIALIZING;

const dbConfig = config.get('db');
const { name, user, password, host, port } = dbConfig;

const sequelize = new Sequelize(name, user, password, {
host,
port,
dialect: 'postgres',
logging: false
});

db = {};

fs.readdirSync(__dirname)
.filter(file => {
return ((file.indexOf('.js') !== 0) &&
(file !== 'db.js') &&
(file !== 'users.js') &&
(file.indexOf('.swp') < 0));
!file.startsWith('.') &&
(file !== 'db.js'));
})
.forEach(file => {
const model = sequelize.import(path.join(__dirname, file));
Expand All @@ -74,7 +72,7 @@ module.exports = () => {
db.sequelize = sequelize;
db.Sequelize = Sequelize;

return db.sequelize.sync().then(() => {
return sequelize.sync().then(() => {
while (deferreds.length) {
deferreds.pop().resolve(db);
}
Expand All @@ -86,4 +84,6 @@ module.exports = () => {
deferreds.pop().reject(e);
}
});
};
}

export { sequelize, Sequelize };
Loading