├── microservices ├── .gitignore ├── auth-proxy │ ├── app.js │ ├── echo-server.js │ └── package.json ├── gateway │ ├── .gitignore │ ├── app.js │ ├── logger.js │ ├── nginx.conf │ └── package.json ├── microservice-1-webtask │ ├── logger.js │ └── server.js └── microservice-1 │ ├── .gitignore │ ├── package.json │ ├── server.js │ └── tickets.json └── twofa ├── Flowchart.png ├── Flowchart.xml └── backend ├── .gitignore ├── app.js ├── bin └── www ├── package.json ├── public └── stylesheets │ └── style.css ├── routes └── routes.js ├── users.json └── views ├── login.hjs ├── strings.json ├── totp-input.hjs └── totp-setup.hjs /microservices/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /microservices/auth-proxy/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var httpProxy = require('http-proxy'); 3 | var jwt = require('jsonwebtoken'); 4 | 5 | // User data. Valid users data that normally gets loaded from a database. 6 | var users = { 7 | "user1": { 8 | username: "user1", 9 | password: "user1pass" 10 | }, 11 | "user2": { 12 | username: "user2", 13 | password: "user2pass" 14 | } 15 | }; 16 | 17 | var secretKey = "super secret jwt key"; 18 | var issuerStr = "Sample API Proxy" 19 | 20 | var proxy = httpProxy.createProxyServer({}); 21 | 22 | function send401(res) { 23 | res.statusCode = 401; 24 | res.end(); 25 | } 26 | 27 | function doLogin(req, res) { 28 | req.on('data', function(chunk) { 29 | try { 30 | var loginData = JSON.parse(chunk); 31 | var user = users[loginData.username]; 32 | if(user && user.password === loginData.password) { 33 | var token = jwt.sign({}, secretKey, { 34 | subject: user.username, 35 | issuer: issuerStr 36 | }); 37 | 38 | res.writeHeader(200, { 39 | 'Content-Length': token.length, 40 | 'Content-Type': "text/plain" 41 | }); 42 | res.write(token); 43 | res.end; 44 | } else { 45 | send401(res); 46 | } 47 | } catch(err) { 48 | console.log(err); 49 | send401(res); 50 | } 51 | }); 52 | } 53 | 54 | function validateAuth(data) { 55 | data = data.split(" "); 56 | if(data[0] !== "Bearer" || !data[1]) { 57 | return false; 58 | } 59 | 60 | var token = data[1]; 61 | try { 62 | var payload = jwt.verify(token, secretKey); 63 | // Custom validation logic, in this case we just check that the 64 | // user exists 65 | if(users[payload.sub]) { 66 | return true; 67 | } 68 | } catch(err) { 69 | console.log(err); 70 | } 71 | 72 | return false; 73 | } 74 | 75 | var server = http.createServer(function(req, res) { 76 | if(req.url === "/login" && req.method === 'POST') { 77 | doLogin(req, res); 78 | return; 79 | } 80 | 81 | var authHeader = req.headers["authorization"]; 82 | if(!authHeader || !validateAuth(authHeader)) { 83 | send401(res); 84 | return; 85 | } 86 | 87 | proxy.web(req, res, { target: "http://127.0.0.1:3001" }); 88 | }); 89 | 90 | console.log("Listening on port 3000"); 91 | server.listen(3000); 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /microservices/auth-proxy/echo-server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | http.createServer(function(req, res) { 4 | console.log(req.url); 5 | console.log(req.headers); 6 | 7 | req.on('data', function(chunk) { 8 | console.log(chunk); 9 | }); 10 | 11 | res.statusCode = 200; 12 | res.end(); 13 | }).listen(3001); 14 | 15 | console.log("Listening on port 3001"); 16 | 17 | 18 | -------------------------------------------------------------------------------- /microservices/auth-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-proxy", 3 | "version": "1.0.0", 4 | "description": "Simple authentication proxy using JWT", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Sebastian Peyrott", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "http-proxy": "^1.11.1", 13 | "jsonwebtoken": "^5.0.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /microservices/gateway/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | db 3 | -------------------------------------------------------------------------------- /microservices/gateway/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var url = require('url'); 3 | var jwt = require('jsonwebtoken'); 4 | var mongoose = require('mongoose'); 5 | var morgan = require('morgan'); 6 | var sprintf = require('sprintf'); 7 | var Q = require('q'); 8 | var _ = require('underscore'); 9 | var amqp = require('amqp'); 10 | 11 | var logger = require('./logger'); 12 | 13 | var amqpHost = process.env.AMQP_HOST || 'amqp://gateway:gateway@127.0.0.1:5672'; 14 | 15 | var httpLogger = morgan('combined', { stream: logger.stream }); 16 | 17 | function toBase64(obj) { 18 | return new Buffer(JSON.stringify(obj)).toString('base64'); 19 | } 20 | 21 | var amqpConn = amqp.createConnection({url: amqpHost}); 22 | 23 | var userDb = mongoose.createConnection(process.env.USER_DB_URL || 24 | 'mongodb://guest:guest@localhost:21017/test/users'); 25 | var servicesDb = mongoose.createConnection(process.env.SERVICES_DB_URL || 26 | 'mongodb://guest:guest@localhost:21017/test/services'); 27 | 28 | // Mongoose user model 29 | var User = userDb.model('User', new mongoose.Schema ({ 30 | username: String, 31 | password: String, 32 | roles: [ String ] 33 | })); 34 | 35 | var Service = servicesDb.model('Service', new mongoose.Schema ({ 36 | name: String, 37 | url: String, 38 | endpoints: [ new mongoose.Schema({ 39 | type: String, 40 | url: String 41 | }) ], 42 | authorizedRoles: [ String ] 43 | })); 44 | 45 | var secretKey = "super secret jwt key"; 46 | var issuerStr = "Sample API Gateway" 47 | 48 | function send401(res) { 49 | res.statusCode = 401; 50 | res.end(); 51 | } 52 | 53 | function send500(res) { 54 | res.statusCode = 500; 55 | res.end(); 56 | } 57 | 58 | /* Get all pending data from HTTP request */ 59 | function getData(req) { 60 | var result = Q.defer(); 61 | 62 | var data = ""; 63 | req.on('data', function(data_) { 64 | data += data_; 65 | if(data.length >= (1024 * 1024)) { 66 | data = ""; 67 | result.reject("Bad request"); 68 | } 69 | }); 70 | 71 | req.on('end', function() { 72 | if(result.promise.isPending()) { 73 | try { 74 | result.resolve(data); 75 | } catch(err) { 76 | result.reject(err.toString()); 77 | } 78 | } 79 | }); 80 | 81 | return result.promise; 82 | } 83 | 84 | /* 85 | * Simple login: returns a JWT if login data is valid. 86 | */ 87 | function doLogin(req, res) { 88 | getData(req).then(function(data) { 89 | try { 90 | var loginData = JSON.parse(data); 91 | User.findOne({ username: loginData.username }, function(err, user) { 92 | if(err) { 93 | logger.error(err); 94 | send401(res); 95 | return; 96 | } 97 | 98 | if(user.password === loginData.password) { 99 | var token = jwt.sign({}, secretKey, { 100 | subject: user.username, 101 | issuer: issuerStr 102 | }); 103 | 104 | res.writeHeader(200, { 105 | 'Content-Length': token.length, 106 | 'Content-Type': "text/plain" 107 | }); 108 | res.write(token); 109 | res.end(); 110 | } else { 111 | send401(res); 112 | } 113 | }, 'users'); 114 | } catch(err) { 115 | logger.error(err); 116 | send401(res); 117 | } 118 | }, function(err) { 119 | logger.error(err); 120 | send401(res); 121 | }); 122 | } 123 | 124 | /* 125 | * Authentication validation using JWT. Strategy: find existing user. 126 | */ 127 | function validateAuth(data, callback) { 128 | if(!data) { 129 | callback(null); 130 | return; 131 | } 132 | 133 | data = data.split(" "); 134 | if(data[0] !== "Bearer" || !data[1]) { 135 | callback(null); 136 | return; 137 | } 138 | 139 | var token = data[1]; 140 | try { 141 | var payload = jwt.verify(token, secretKey); 142 | // Custom validation logic, in this case we just check that the 143 | // user exists 144 | User.findOne({ username: payload.sub }, function(err, user) { 145 | if(err) { 146 | logger.error(err); 147 | } else { 148 | callback({ 149 | user: user, 150 | jwt: payload 151 | }); 152 | } 153 | }); 154 | } catch(err) { 155 | logger.error(err); 156 | callback(null); 157 | } 158 | } 159 | 160 | /* 161 | * Internal HTTP request, auth data is passed in headers. 162 | */ 163 | function httpSend(oldReq, endpoint, data, deferred, isGet) { 164 | var parsedEndpoint = url.parse(endpoint); 165 | 166 | var options = { 167 | hostname: parsedEndpoint.hostname, 168 | port: parsedEndpoint.port, 169 | path: parsedEndpoint.path, 170 | method: isGet ? 'GET' : 'POST', 171 | headers: isGet ? {} : { 172 | 'Content-Type': 'application/json', 173 | 'Content-Length': data.length, 174 | 'GatewayAuth': toBase64(oldReq.authPayload) 175 | } 176 | }; 177 | 178 | var req = http.request(options, function(res) { 179 | var resData = ""; 180 | res.on('data', function (chunk) { 181 | resData += chunk; 182 | }); 183 | res.on('end', function() { 184 | try { 185 | var json = JSON.parse(resData); 186 | deferred.resolve(json); 187 | } catch(err) { 188 | deferred.reject({ 189 | req: oldReq, 190 | endpoint: endpoint, 191 | message: 'Invalid data format: ' + err.toString() 192 | }); 193 | } 194 | }); 195 | }); 196 | 197 | req.on('error', function(e) { 198 | deferred.reject({ 199 | req: oldReq, 200 | endpoint: endpoint, 201 | message: e.toString() 202 | }); 203 | }); 204 | 205 | if(!isGet && data) { 206 | req.write(data); 207 | } 208 | req.end(); 209 | } 210 | 211 | /* 212 | * Internal HTTP request 213 | */ 214 | function httpPromise(req, endpoint, isGet) { 215 | var result = Q.defer(); 216 | 217 | function reject(msg) { 218 | result.reject({ 219 | req: req, 220 | endpoint: endpoint, 221 | message: msg 222 | }); 223 | } 224 | 225 | if(isGet) { 226 | httpSend(req, endpoint, null, result, isGet); 227 | } else { 228 | getData(req).then(function(data) { 229 | httpSend(req, endpoint, data, result, isGet); 230 | }, function(err) { 231 | reject(err); 232 | }); 233 | } 234 | 235 | return result.promise; 236 | } 237 | 238 | function amqpSend(req, endpoint, data, result) { 239 | amqpConn.queue('', { 240 | exclusive: true 241 | }, function(queue) { 242 | queue.bind('#'); 243 | 244 | queue.subscribe({ ack: true, prefetchCount: 1 }, 245 | function(message, headers, deliveryInfo, messageObject) { 246 | messageObject.acknowledge(); 247 | 248 | try { 249 | var json = JSON.parse(message); 250 | deferred.resolve(json); 251 | } catch(err) { 252 | deferred.reject({ 253 | req: req, 254 | endpoint: endpoint, 255 | message: 'Invalid data format: ' + err.toString() 256 | }); 257 | } 258 | } 259 | ); 260 | 261 | //Default exchange 262 | var exchange = amqpConn.exchange(); 263 | //Send data 264 | exchange.publish(endpoint, data ? data : {}, { 265 | headers: { 266 | 'GatewayAuth': toBase64(req.authPayload), 267 | }, 268 | deliveryMode: 1, //non-persistent 269 | replyTo: queue.name, 270 | mandatory: true, 271 | immediate: true 272 | }, function(err) { 273 | if(err) { 274 | deferred.reject({ 275 | req: req, 276 | endpoint: endpoint, 277 | message: 'Could not publish message to the default ' + 278 | 'AMQP exchange' 279 | }); 280 | } 281 | }); 282 | }); 283 | } 284 | 285 | /* 286 | * Internal AMQP request 287 | */ 288 | function amqpPromise(req, endpoint, isGet) { 289 | var result = Q.defer(); 290 | 291 | function reject(msg) { 292 | result.reject({ 293 | req: req, 294 | endpoint: endpoint, 295 | message: msg 296 | }); 297 | } 298 | 299 | if(req.method === 'POST') { 300 | getData(req).then(function(data) { 301 | amqpSend(req, endpoint, data, result); 302 | }, function(err) { 303 | reject(err); 304 | }); 305 | } else { 306 | amqpSend(req, endpoint, null, result); 307 | } 308 | 309 | return result.promise; 310 | } 311 | 312 | function roleCheck(user, service) { 313 | var intersection = _.intersection(user.roles, service.authorizedRoles); 314 | return intersection.length === service.authorizedRoles.length; 315 | } 316 | 317 | /* 318 | * Parses the request and dispatches multiple concurrent requests to each 319 | * internal endpoint. Results are aggregated and returned. 320 | */ 321 | function serviceDispatch(req, res) { 322 | var parsedUrl = url.parse(req.url); 323 | 324 | Service.findOne({ url: parsedUrl.pathname }, function(err, service) { 325 | if(err) { 326 | logger.error(err); 327 | send500(res); 328 | return; 329 | } 330 | 331 | var authorized = roleCheck(req.context.authPayload.user, service); 332 | if(!authorized) { 333 | send401(res); 334 | return; 335 | } 336 | 337 | // Fanout all requests to all related endpoints. 338 | // Results are aggregated (more complex strategies are possible). 339 | var promises = []; 340 | service.endpoints.forEach(function(endpoint) { 341 | logger.debug(sprintf('Dispatching request from public endpoint ' + 342 | '%s to internal endpoint %s (%s)', 343 | req.url, endpoint.url, endpoint.type)); 344 | 345 | switch(endpoint.type) { 346 | case 'http-get': 347 | case 'http-post': 348 | promises.push(httpPromise(req, endpoint.url, 349 | endpoint.type === 'http-get')); 350 | break; 351 | case 'amqp': 352 | promises.push(amqpPromise(req, endpoint.url)); 353 | break; 354 | default: 355 | logger.error('Unknown endpoint type: ' + endpoint.type); 356 | } 357 | }); 358 | 359 | //Aggregation strategy for multiple endpoints. 360 | Q.allSettled(promises).then(function(results) { 361 | var responseData = {}; 362 | 363 | results.forEach(function(result) { 364 | if(result.state === 'fulfilled') { 365 | responseData = _.extend(responseData, result.value); 366 | } else { 367 | logger.error(result.reason.message); 368 | } 369 | }); 370 | 371 | res.setHeader('Content-Type', 'application/json'); 372 | res.end(JSON.stringify(responseData)); 373 | }); 374 | }, 'services'); 375 | } 376 | 377 | var server = http.createServer(function(req, res) { 378 | httpLogger(req, res, function(){}); 379 | 380 | // Login endpoint 381 | if(req.url === "/login" && req.method === 'POST') { 382 | doLogin(req, res); 383 | return; 384 | } 385 | 386 | // Authentication 387 | var authHeader = req.headers["authorization"]; 388 | validateAuth(authHeader, function(authPayload) { 389 | if(!authPayload) { 390 | send401(res); 391 | return; 392 | } 393 | 394 | // We keep the authentication payload to pass it to 395 | // microservices decoded. 396 | req.context = { 397 | authPayload: authPayload 398 | }; 399 | 400 | serviceDispatch(req, res); 401 | }); 402 | }); 403 | 404 | logger.info("Listening on port 3000"); 405 | server.listen(3000); 406 | 407 | 408 | 409 | 410 | -------------------------------------------------------------------------------- /microservices/gateway/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | var winstonAmqp = require('winston-amqp'); 3 | 4 | var amqpHost = process.env.AMQP_HOST || 'amqp://gateway:gateway@127.0.0.1:5672'; 5 | 6 | winston.emitErrs = true; 7 | var logger = new winston.Logger({ 8 | transports: [ 9 | new winston.transports.Console({ 10 | timestamp: true, 11 | level: process.env.GATEWAY_LOG_LEVEL || 'debug', 12 | handleExceptions: false, 13 | json: false, 14 | colorize: true 15 | }), 16 | new winstonAmqp.AMQP({ 17 | name: 'gateway', 18 | level: process.env.GATEWAY_LOG_LEVEL || 'debug', 19 | host: amqpHost, 20 | exchange: 'log', 21 | routingKey: 'gateway' 22 | }) 23 | ], 24 | exitOnError: false 25 | }); 26 | 27 | logger.stream = { 28 | write: function(message, encoding) { 29 | logger.debug(message.replace(/\n$/, '')); 30 | } 31 | }; 32 | 33 | module.exports = logger; 34 | 35 | 36 | -------------------------------------------------------------------------------- /microservices/gateway/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | #user gateway; 3 | worker_processes 1; 4 | 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | include mime.types; 11 | default_type application/json; 12 | 13 | keepalive_timeout 65; 14 | 15 | server { 16 | listen 443 ssl; 17 | server_name yourdomain.com; 18 | 19 | ssl_certificate cert.pem; 20 | ssl_certificate_key cert.key; 21 | 22 | ssl_session_cache shared:SSL:1m; 23 | ssl_session_timeout 5m; 24 | 25 | ssl_ciphers HIGH:!aNULL:!MD5; 26 | ssl_prefer_server_ciphers on; 27 | 28 | location public1.yourdomain.com { 29 | proxy_pass http://localhost:9000; 30 | } 31 | 32 | location public2.yourdomain.com { 33 | proxy_pass http://localhost:9001; 34 | } 35 | 36 | location public3.yourdomain.com { 37 | proxy_pass http://localhost:9002; 38 | } 39 | } 40 | } 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /microservices/gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gateway", 3 | "version": "1.0.0", 4 | "description": "API gateway example", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "amqp": "^0.2.4", 13 | "amqp-winston": "^1.0.7", 14 | "http-proxy": "^1.11.2", 15 | "jsonwebtoken": "^5.0.5", 16 | "mongoose": "^4.1.5", 17 | "morgan": "^1.6.1", 18 | "q": "^1.4.1", 19 | "sprintf": "^0.1.5", 20 | "underscore": "^1.8.3", 21 | "winston": "^1.0.1", 22 | "winston-amqp": "0.0.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /microservices/microservice-1-webtask/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | winston.emitErrs = true; 3 | 4 | var logger = new winston.Logger({ 5 | transports: [ 6 | new winston.transports.Console({ 7 | timestamp: true, 8 | level: 'debug', 9 | handleExceptions: true, 10 | json: false, 11 | colorize: true 12 | }) 13 | ], 14 | exitOnError: false 15 | }); 16 | 17 | module.exports = logger; 18 | module.exports.stream = { 19 | write: function(message, encoding){ 20 | logger.debug(message.replace(/\n$/, '')); 21 | } 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /microservices/microservice-1-webtask/server.js: -------------------------------------------------------------------------------- 1 | var webtask = require('webtask-tools'); 2 | var express = require('express'); 3 | var morgan = require('morgan'); 4 | var mongo = require('mongodb').MongoClient; 5 | var winston = require('winston'); 6 | 7 | // Logging 8 | winston.emitErrs = true; 9 | var logger = new winston.Logger({ 10 | transports: [ 11 | new winston.transports.Console({ 12 | timestamp: true, 13 | level: 'debug', 14 | handleExceptions: true, 15 | json: false, 16 | colorize: true 17 | }) 18 | ], 19 | exitOnError: false 20 | }); 21 | 22 | logger.stream = { 23 | write: function(message, encoding){ 24 | logger.debug(message.replace(/\n$/, '')); 25 | } 26 | }; 27 | 28 | // Express and middlewares 29 | var app = express(); 30 | app.use( 31 | //Log requests 32 | morgan(':method :url :status :response-time ms - :res[content-length]', { 33 | stream: logger.stream 34 | }) 35 | ); 36 | 37 | var db; 38 | if(process.env.MONGO_URL) { 39 | mongo.connect(process.env.MONGO_URL, null, function(err, db_) { 40 | if(err) { 41 | logger.error(err); 42 | } else { 43 | db = db_; 44 | } 45 | }); 46 | } 47 | 48 | app.use(function(req, res, next) { 49 | if(!db) { 50 | //Database not connected 51 | mongo.connect(process.env.MONGO_URL || 52 | req.webtaskContext.data.MONGO_URL, null, 53 | function(err, db_) { 54 | if(err) { 55 | logger.error(err); 56 | res.sendStatus(500); 57 | } else { 58 | db = db_; 59 | next(); 60 | } 61 | } 62 | ); 63 | } else { 64 | next(); 65 | } 66 | }); 67 | 68 | // Actual query 69 | app.get('/tickets', function(req, res, next) { 70 | var collection = db.collection('tickets'); 71 | collection.find().toArray(function(err, result) { 72 | if(err) { 73 | logger.error(err); 74 | res.sendStatus(500); 75 | return; 76 | } 77 | res.json(result); 78 | }); 79 | }); 80 | 81 | //Express to webtask adapter 82 | module.exports = require('webtask-tools').fromExpress(app); 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /microservices/microservice-1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | db 3 | -------------------------------------------------------------------------------- /microservices/microservice-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microservice-1", 3 | "version": "1.0.0", 4 | "description": "Sample microservice, queries \"tickets\"", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "express": "^4.13.3", 13 | "mongodb": "^2.0.42", 14 | "morgan": "^1.6.1", 15 | "winston": "^1.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /microservices/microservice-1/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var morgan = require('morgan'); 3 | var http = require('http'); 4 | var mongo = require('mongodb').MongoClient; 5 | var winston = require('winston'); 6 | 7 | // Logging 8 | winston.emitErrs = true; 9 | var logger = new winston.Logger({ 10 | transports: [ 11 | new winston.transports.Console({ 12 | timestamp: true, 13 | level: 'debug', 14 | handleExceptions: true, 15 | json: false, 16 | colorize: true 17 | }) 18 | ], 19 | exitOnError: false 20 | }); 21 | 22 | logger.stream = { 23 | write: function(message, encoding){ 24 | logger.debug(message.replace(/\n$/, '')); 25 | } 26 | }; 27 | 28 | // Express and middlewares 29 | var app = express(); 30 | app.use( 31 | //Log requests 32 | morgan(':method :url :status :response-time ms - :res[content-length]', { 33 | stream: logger.stream 34 | }) 35 | ); 36 | 37 | var db; 38 | if(process.env.MONGO_URL) { 39 | mongo.connect(process.env.MONGO_URL, null, function(err, db_) { 40 | if(err) { 41 | logger.error(err); 42 | } else { 43 | db = db_; 44 | } 45 | }); 46 | } 47 | 48 | app.use(function(req, res, next) { 49 | if(!db) { 50 | //Database not connected 51 | mongo.connect(process.env.MONGO_URL, null, function(err, db_) { 52 | if(err) { 53 | logger.error(err); 54 | res.sendStatus(500); 55 | } else { 56 | db = db_; 57 | next(); 58 | } 59 | }); 60 | } else { 61 | next(); 62 | } 63 | }); 64 | 65 | // Actual query 66 | app.get('/tickets', function(req, res, next) { 67 | var collection = db.collection('tickets'); 68 | collection.find().toArray(function(err, result) { 69 | if(err) { 70 | logger.error(err); 71 | res.sendStatus(500); 72 | return; 73 | } 74 | res.json(result); 75 | }); 76 | }); 77 | 78 | // Standalone server setup 79 | var port = process.env.PORT || 3001; 80 | http.createServer(app).listen(port, function (err) { 81 | if (err) { 82 | logger.error(err); 83 | } else { 84 | logger.info('Listening on http://localhost:' + port); 85 | } 86 | }); 87 | 88 | 89 | -------------------------------------------------------------------------------- /microservices/microservice-1/tickets.json: -------------------------------------------------------------------------------- 1 | {"_id":{"$oid":"55e633b9fa7864d21a226052"},"assignedTo":"gonto","description":"Hey there, I would like my users to authenticate with Facebook and Twitter. I'm using AngularJS, how can I do this?","id":1000.0,"replies":[{"message":"\u003cp\u003eHi John, thanks for reaching out.\u003c/p\u003e\u003cp\u003eHave you seen our library for AngularJS?\u003c/p\u003e\u003cp\u003e\u003ca href=\"https://github.com/auth0/auth0-angular\"\u003ehttps://github.com/auth0/auth0-angular\u003c/a\u003e\u003c/p\u003e","user":"Gonto"}],"shortDescription":"Hey there, I would like my users to authenticate with....","status":"Open","title":"Social Authentication","userInitials":"JD"} 2 | {"_id":{"$oid":"55e633dcfa7864d21a226053"},"assignedTo":"","description":"Hey there, we would like to use Active Directory authentication in our iOS app, can you help?","id":1001.0,"shortDescription":"Hey there, we would like to use Active Directory...","status":"Open","title":"AD on iOS","userInitials":"KL"} 3 | {"_id":{"$oid":"55e633f0fa7864d21a226054"},"assignedTo":"","description":"Auth0 looks really useful but at the moment we're still in development phase. Is there any way we can use a free account or something similar?","id":1002.0,"replies":[{"message":"\u003cp\u003eHi Roger, thanks for reaching out.\u003c/p\u003e\u003cp\u003eOn auth0.com you can simply sign up for a free developer account. You will only need to upgrade this account to a paid account whenever you move to production.\u003c/p\u003e","user":"Gonto"}],"shortDescription":"Auth0 looks really useful but at the moment we're still in...","status":"Open","title":"Developer account?","userInitials":"R"} 4 | -------------------------------------------------------------------------------- /twofa/Flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mfa-and-microservices-blog-samples/d3fd5cc0a6a0d187a6efd12128b72eba425aa499/twofa/Flowchart.png -------------------------------------------------------------------------------- /twofa/Flowchart.xml: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /twofa/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Debug log from npm 30 | npm-debug.log 31 | node_modules 32 | -------------------------------------------------------------------------------- /twofa/backend/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var cookieParser = require('cookie-parser'); 4 | var bodyParser = require('body-parser'); 5 | var session = require('express-session'); 6 | 7 | var base32 = require('thirty-two'); 8 | var sprintf = require('sprintf'); 9 | var crypto = require('crypto'); 10 | 11 | var passport = require('passport'); 12 | var LocalStrategy = require('passport-local').Strategy; 13 | var TotpStrategy = require('passport-totp').Strategy; 14 | 15 | var strings = require('./views/strings.json'); 16 | 17 | /* PERSISTENT STORAGE */ 18 | var fs = require('fs'); 19 | // Load users from a persistent store (a DB is what you normally use here). 20 | // For the sake of simplicity we will use a JSON file storing usernames, 21 | // passwords and secrets. DO NOT DO THIS in production, passwords must 22 | // never be stored as plain text. 23 | var users = {}; 24 | try { 25 | users = JSON.parse(fs.readFileSync('users.json', { encoding: "utf8" })); 26 | } catch(e) { 27 | //Do nothing, keep users empty 28 | } 29 | /* END: PERSISTENT STORAGE */ 30 | 31 | function verifyCredentials(username, password) { 32 | console.log(users); 33 | var user = users[username]; 34 | if(!user) { 35 | return false; 36 | } 37 | 38 | return user.password === password; 39 | } 40 | 41 | passport.use(new LocalStrategy( 42 | function(username, password, done) { 43 | var valid = verifyCredentials(username, password); 44 | return done(null, valid ? users[username] : false); 45 | }) 46 | ); 47 | 48 | passport.use(new TotpStrategy( 49 | function(user, done) { 50 | var key = user.key; 51 | if(!key) { 52 | return done(new Error('No key')); 53 | } else { 54 | return done(null, base32.decode(key), 30); //30 = valid key period 55 | } 56 | }) 57 | ); 58 | 59 | passport.serializeUser(function(user, done) { 60 | done(null, user.id); 61 | }); 62 | 63 | passport.deserializeUser(function(id, done) { 64 | for(var u in users) { 65 | if(users[u].id === id) { 66 | done(null, users[u]); 67 | return; 68 | } 69 | } 70 | 71 | done(new Error("Not found")); 72 | }); 73 | 74 | var app = express(); 75 | 76 | // view engine setup 77 | app.set('views', path.join(__dirname, 'views')); 78 | app.set('view engine', 'hjs'); 79 | 80 | app.use(bodyParser.json()); 81 | app.use(bodyParser.urlencoded({ extended: false })); 82 | app.use(cookieParser()); 83 | app.use(express.static(path.join(__dirname, 'public'))); 84 | app.use(session({ secret: 'totp test app secret' })); 85 | app.use(passport.initialize()); 86 | app.use(passport.session()); 87 | 88 | function isLoggedIn(req, res, next) { 89 | if(req.isAuthenticated()) { 90 | next(); 91 | } else { 92 | res.redirect('/login'); 93 | } 94 | } 95 | 96 | function ensureTotp(req, res, next) { 97 | if((req.user.key && req.session.method == 'totp') || 98 | (!req.user.key && req.session.method == 'plain')) { 99 | next(); 100 | } else { 101 | res.redirect('/login'); 102 | } 103 | } 104 | 105 | app.get('/', isLoggedIn, ensureTotp, function(req, res) { 106 | res.redirect('/totp-setup'); 107 | }); 108 | 109 | app.get('/login', function(req, res) { 110 | req.logout(); 111 | res.render('login', { 112 | strings: strings 113 | }); 114 | }); 115 | 116 | app.post('/login', 117 | passport.authenticate('local', { failureRedirect: '/login' }), 118 | function(req, res) { 119 | if(req.user.key) { 120 | req.session.method = 'totp'; 121 | res.redirect('/totp-input'); 122 | } else { 123 | req.session.method = 'plain'; 124 | res.redirect('/totp-setup'); 125 | } 126 | } 127 | ); 128 | 129 | app.get('/totp-input', isLoggedIn, function(req, res) { 130 | if(!req.user.key) { 131 | console.log("Logic error, totp-input requested with no key set"); 132 | res.redirect('/login'); 133 | } 134 | 135 | res.render('totp-input', { 136 | strings: strings 137 | }); 138 | }); 139 | 140 | app.post('/totp-input', isLoggedIn, passport.authenticate('totp', { 141 | failureRedirect: '/login', 142 | successRedirect: '/totp-setup' 143 | })); 144 | 145 | app.get('/totp-setup', 146 | isLoggedIn, 147 | ensureTotp, 148 | function(req, res) { 149 | var url = null; 150 | if(req.user.key) { 151 | var qrData = sprintf('otpauth://totp/%s?secret=%s', 152 | req.user.username, req.user.key); 153 | url = "https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=" + 154 | qrData; 155 | } 156 | 157 | res.render('totp-setup', { 158 | strings: strings, 159 | user: req.user, 160 | qrUrl: url 161 | }); 162 | } 163 | ); 164 | 165 | app.post('/totp-setup', 166 | isLoggedIn, 167 | ensureTotp, 168 | function(req, res) { 169 | if(req.body.totp) { 170 | req.session.method = 'totp'; 171 | 172 | var secret = base32.encode(crypto.randomBytes(16)); 173 | //Discard equal signs (part of base32, 174 | //not required by Google Authenticator) 175 | //Base32 encoding is required by Google Authenticator. 176 | //Other applications 177 | //may place other restrictions on the shared key format. 178 | secret = secret.toString().replace(/=/g, ''); 179 | req.user.key = secret; 180 | } else { 181 | req.session.method = 'plain'; 182 | 183 | req.user.key = null; 184 | } 185 | 186 | res.redirect('/totp-setup'); 187 | } 188 | ); 189 | 190 | // error handlers 191 | 192 | // catch 404 and forward to error handler 193 | app.use(function(req, res, next) { 194 | var err = new Error('Not Found'); 195 | err.status = 404; 196 | next(err); 197 | }); 198 | 199 | var errorHandler = function(err, req, res, next){ 200 | console.log(err.stack); 201 | res.send(500); 202 | // or you could call res.render('error'); if you have a view for that. 203 | }; 204 | 205 | app.use(errorHandler); 206 | 207 | 208 | // Exit handler 209 | function onExit() { 210 | try { 211 | fs.writeFileSync('users.json', JSON.stringify(users), { 212 | encoding: "utf8" 213 | }); 214 | } catch(e) { 215 | console.log(e); 216 | } 217 | 218 | process.exit(); 219 | } 220 | 221 | process.on('exit', onExit); 222 | process.on('SIGINT', onExit); 223 | 224 | module.exports = app; 225 | 226 | 227 | -------------------------------------------------------------------------------- /twofa/backend/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('backend:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /twofa/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cookie-parser": "~1.3.5", 11 | "debug": "~2.2.0", 12 | "express": "~4.13.1", 13 | "express-json": "^1.0.0", 14 | "express-session": "^1.11.3", 15 | "hjs": "~0.0.6", 16 | "morgan": "~1.6.1", 17 | "passport": "^0.2.2", 18 | "passport-local": "^1.0.0", 19 | "passport-totp": "0.0.1", 20 | "serve-favicon": "~2.3.0", 21 | "sprintf": "^0.1.5", 22 | "thirty-two": "0.0.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /twofa/backend/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /twofa/backend/routes/routes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/mfa-and-microservices-blog-samples/d3fd5cc0a6a0d187a6efd12128b72eba425aa499/twofa/backend/routes/routes.js -------------------------------------------------------------------------------- /twofa/backend/users.json: -------------------------------------------------------------------------------- 1 | {"test":{"id":1,"username":"test","password":"pass","key":null}} -------------------------------------------------------------------------------- /twofa/backend/views/login.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ strings.appTitle }} 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /twofa/backend/views/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "TOTP Test", 3 | "username": "Username", 4 | "password": "Password", 5 | "enterCode": "Generated Code", 6 | "qrSetupSteps": "Scan this code with Google Authenticator", 7 | "welcome": "Welcome", 8 | "enableTotp": "Enable TOTP" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /twofa/backend/views/totp-input.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ strings.appTitle }} 5 | 6 | 7 | 8 |
9 |
10 |
11 |

{{ strings.enterCode }}

12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /twofa/backend/views/totp-setup.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ strings.appTitle }} 5 | 6 | 7 | 8 |
9 |
10 |
11 |

{{strings.welcome}}, {{user.username}}

12 |

{{ strings.enableTotp }}

13 | {{^user.key}} 14 |
15 | 16 |
17 | {{/user.key}} 18 | {{#user.key}} 19 |
20 | 21 |
22 | 23 |

{{strings.qrSetupSteps}}

24 | 25 | {{ user.key }} 26 | {{/user.key}} 27 | 28 |
29 | 30 | 31 | 32 | 33 | --------------------------------------------------------------------------------