-
Notifications
You must be signed in to change notification settings - Fork 1
[wip] Example Frontend #21
base: master
Are you sure you want to change the base?
Changes from all commits
8af21c4
9575800
369a871
ae18821
ec4f6df
9a30f02
637f968
408128b
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 |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| const express = require('express'); | ||
| const helmet = require('helmet') | ||
| 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'); | ||
|
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. do you just not like it saying express, or is this an actual security thing nowadays?
Contributor
Author
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. Security thing On Saturday, June 28, 2014, Chris McAvoy notifications@github.com wrote:
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); | ||
|
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. 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; | ||
| 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); | ||
| }); |
| 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'); | ||
| } |
| 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; | ||
| } |
| 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; |
| 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", | ||
|
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. 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" | ||
| } | ||
| } | ||
Large diffs are not rendered by default.
| 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)) |
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.
just read the docs on this...it looks great