Skip to content
This repository was archived by the owner on Sep 2, 2020. It is now read-only.
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
81 changes: 81 additions & 0 deletions client/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const express = require('express');
const helmet = require('helmet')
const path = require('path');
const logger = require('morgan');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

just read the docs on this...it looks great

const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const session = require('client-sessions');
const csrf = require('csurf');
const methodOverride = require('method-override');

const routes = require('./routes/index');
const users = require('./routes/users');

const app = express();
app.set('view engine', 'html');
app.engine('html', require('hbs').__express);

app.disable('x-powered-by');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do you just not like it saying express, or is this an actual security thing nowadays?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Security thing

On Saturday, June 28, 2014, Chris McAvoy notifications@github.com wrote:

In client/app.js:

+const path = require('path');
+const logger = require('morgan');
+const cookieParser = require('cookie-parser');
+const bodyParser = require('body-parser');
+const session = require('client-sessions');
+const csrf = require('csurf');
+const methodOverride = require('method-override');
+
+const routes = require('./routes/index');
+const users = require('./routes/users');
+
+const app = express();
+app.set('view engine', 'html');
+app.engine('html', require('hbs').__express);
+
+app.disable('x-powered-by');

do you just not like it saying express, or is this an actual security
thing nowadays?

—
Reply to this email directly or view it on GitHub
https://github.com/mozilla/badgekit-backpack/pull/21/files#r14324978.

Brian


app.use(helmet.csp({
'script-src': ["'self'", 'https://login.persona.org'],
'style-src': ["'self'", "'unsafe-inline'"],
}));
app.use(helmet.xframe('deny'));
app.use(helmet.iexss());
app.use(helmet.contentTypeOptions());
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(methodOverride(function (req, res) {
if (req.body && typeof req.body === 'object' && '_method' in req.body) {
var method = req.body._method;
delete req.body._method;
return method;
}
}));
app.use(cookieParser());
app.use(session({
cookieName: 'backpack::session',
requestKey: 'session',
secret: process.env.COOKIE_SECRET,
duration: 30 * 24 * 60 * 60 * 1000, // 30 days
}))
app.use(csrf());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', routes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I like this Express 4'ness. Excited to start reusing some django-ish-apps across projects.

app.use('/users', users);

/// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

/// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});


module.exports = app;
9 changes: 9 additions & 0 deletions client/bin/www
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env node
var debug = require('debug')('client');
var app = require('../app');

app.set('port', process.env.PORT || 3000);

var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});
52 changes: 52 additions & 0 deletions client/lib/api-authorization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module.exports = createAuthHeader;

const util = require('util');
const crypto = require('crypto');
const jws = require('jws');

/**
* Create an authorization token suitable for providing as the
* `Authorization` header for a given request.
*
* @param {Object} opts Object containing the following properties:
* method: The HTTP method of the outgoing request (e.g, 'POST')
* path: The relative path of the request, including the query string
* if there is one.
* body: The body of the request, if there is one. This can be omitted
* for methods that don't take a body. Must be provided as a buffer
* or a string.
* apiKey: (optional) API key to use. Falls back to `API_KEY`
* environment variable, defaults to "master"
* apiSecret: (optional) Secret to sign the token with. Falls back to
* `API_SECRET` environment variable.
*
* @return Authorization token
*/

function createAuthHeader(opts) {
const apiKey = opts.apiKey || process.env.API_KEY || 'master';
const apiSecret = opts.apiSecret || process.env.API_SECRET;

const payload = {
key: apiKey,
method: opts.method,
path: opts.path,
};

if (opts.body) {
payload.body = {
alg: 'sha256',
hash: sha256(Buffer(opts.body)),
};
}

return util.format('JWT token="%s"', jws.sign({
secret: apiSecret,
header: {typ: 'JWT', alg: 'HS256'},
payload: payload,
}));
}

function sha256(input) {
return crypto.createHash('sha256').update(input).digest('hex');
}
154 changes: 154 additions & 0 deletions client/lib/api-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
module.exports = APIClient;

const Promise = require('bluebird');
const fmt = require('util').format;
const http = require('http');
const https = require('https');
const url = require('url');
const concat = require('concat-stream');
const createAuthHeader = require('./api-authorization');

function APIClient(opts) {
opts = opts || {};
this.secret = opts.secret || process.env.API_SECRET;
this.key = opts.key || process.env.API_KEY || 'master';
this.host = opts.host || process.env.API_HOST;
this.defaultLimit = opts.defaultLimit;

if (!this.secret)
throw new RangeError('Must pass a `secret` or set API_SECRET environment variable');

if (!this.host)
throw new RangeError('Must pass a `host` or set API_HOST environment variable');

var hostUrlObj;
try {
hostUrlObj = url.parse(this.host)
} catch(error) {
throw new TypeError('Could not parse `host` as a valid URL, make sure it is a string');
}

if (!hostUrlObj.protocol)
throw new RangeError('`host` must be a valid fully qualified URL');
}

APIClient.prototype = {
request: function request(opts) {
const host = opts.host || this.host;
const method = opts.method;
const body = opts.body;
const path = opts.path;

const protocol = isHttps(host) ? https : http;
const reqFn = protocol.request.bind(protocol);
const fullUrl = host + opts.path;
const reqOpts = url.parse(fullUrl);

reqOpts.method = method;
reqOpts.headers = {
'content-type': 'application/json',
'authorization': createAuthHeader({
method: method,
path: path,
body: body || null,
apiSecret: this.secret,
apiKey: this.key,
})
}

if (opts.body)
reqOpts.headers['content-length'] = Buffer(body).length;

return new Promise(function (resolve, reject) {
var req;
try {
req = reqFn(reqOpts, responseHandler);
} catch(err) {
return reject(err);
}

if (req.body) {
req.write(body);
}

req.end();

req.on('error', reject);


function responseHandler(res) {
res.setEncoding('utf8');
res.on('error', reject);
res.pipe(concat(function (data) {
var responseBody;

try {
responseBody = JSON.parse(data);
} catch(err){
return reject(new TypeError('Could not parse response as JSON'));
}

if (res.statusCode >= 400) {
return reject(new APIClient.BadResponse({
statusCode: res.statusCode,
response: responseBody,
}));
}

return resolve(responseBody);
}))
}

})

},

getAllBadges: function getAllBadges(opts) {
const host = opts.host || this.host;
const user = opts.user;
const limit = opts.limit || this.defaultLimit || 0;
const page = opts.page || 1;

const method = 'GET';
const path = fmt('/users/%s/badges?limit=%s&page=%s',
user, limit, page);

return this.request({path: path, method: method});
},

getOneBadge: function getOneBadge(opts) {
const host = opts.host || process.env.API_HOST;
const user = opts.user;
const badgeId = opts.badgeId;

const method = 'GET';
const path = fmt('/users/%s/badges/%s', user, badgeId);

return this.request({path: path, method: method});
},

deleteBadge:function deleteBadge(opts) {
const host = opts.host || this.host;
const user = opts.user;
const badgeId = opts.badgeId;

const method = 'DELETE';
const path = fmt('/users/%s/badges/%s', user, badgeId);

return this.request({path: path, method: method});
},
}

APIClient.BadResponse = function BadResponse (obj) {
this.message = "Received a bad server response";
this.statusCode = obj.statusCode;
this.response = obj.response;
}

APIClient.BadResponse.prototype = Object.create(Error.prototype);



function isHttps(url) {
return url.indexOf('https://') === 0;
}
35 changes: 35 additions & 0 deletions client/lib/persona.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module.exports = verify;

const Promise = require('bluebird');
const request = require('request');
const util = require('util');

const AUDIENCE = process.env['HOST'];

if (!AUDIENCE)
throw new Error('Must have a `HOST` environment variable set');

function verify(assertion) {
return new Promise(function (resolve, reject) {
request.post('https://verifier.login.persona.org/verify', {
json: true,
form: {
assertion: assertion,
audience: AUDIENCE
}
}, function (err, res, body) {
if (err)
return reject(err);

if (body.status != 'okay')
return reject(new Error(util.format('could not verify: %j', body)));

if (body.audience != AUDIENCE)
return reject(new Error('could not verify, invalid audience'));

return resolve(body.email);
});
});
}

verify.audience = AUDIENCE;
25 changes: 25 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "client",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"bluebird": "^2.1.3",
"body-parser": "~1.0.0",
"client-sessions": "^0.6.0",
"concat-stream": "^1.4.6",
"cookie-parser": "~1.0.1",
"csurf": "^1.2.2",
"debug": "~0.7.4",
"express": "~4.2.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

420

"hbs": "^2.7.0",
"helmet": "^0.2.4",
"jws": "^0.2.5",
"method-override": "^2.0.2",
"mocha": "^1.20.1",
"morgan": "~1.0.0",
"request": "^2.36.0"
}
}
4 changes: 4 additions & 0 deletions client/public/javascripts/jquery-2.1.1.min.js

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions client/public/javascripts/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*global jQuery*/
(function ($) {
const $login = $('.js-persona-login')
const $form = $('.js-persona-form')
const $assertion = $('.js-persona-assertion')

const user = $form.data('user')

$login.on('click', function (evt) {
navigator.id.request()
})

navigator.id.watch({
loggedInUser: user || null,
onlogin: function (assertion) {
$assertion.val(assertion)
$form.attr('action', $form.data('login-url'))
$form.submit()
},
onlogout: function () {
// we don't actually want to do anything since we're not being ajaxy
// $form.attr('action', $form.data('logout-url'))
// $form.submit()
}
})

}(jQuery))
Loading