├── movieanalyst-website ├── public │ └── views │ │ ├── pending.ejs │ │ ├── publications.ejs │ │ ├── authors.ejs │ │ ├── movies.ejs │ │ └── index.ejs ├── package.json └── server.js ├── README.md ├── movieanalyst-admin ├── package.json ├── public │ └── views │ │ ├── includes │ │ └── navbar.ejs │ │ ├── publications.ejs │ │ ├── authors.ejs │ │ ├── movies.ejs │ │ ├── pending.ejs │ │ └── index.ejs └── server.js ├── movieanalyst-api ├── package.json └── server.js └── .gitignore /movieanalyst-website/public/views/pending.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building and Securing a Modern Backend API 2 | 3 | This repo contains the completed demo code for the article that describes how to build and secure a modern backend API on scotch.io. 4 | 5 | Read the post to follow along here. 6 | 7 | To get the app up and running you will need to have an Auth0 account. You can sign up for one [here](https://auth0.com/signup?utm_source=scotch.io&utm_medium=sp&utm_campaign=api_authorization). 8 | 9 | -------------------------------------------------------------------------------- /movieanalyst-admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "ejs": "^2.5.1", 14 | "express": "^4.14.0", 15 | "superagent": "^1.8.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /movieanalyst-website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "ejs": "^2.5.1", 14 | "express": "^4.14.0", 15 | "superagent": "^1.8.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /movieanalyst-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movieanalyst", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "auth0-api-jwt-rsa-validation": "0.0.1", 14 | "express": "^4.14.0", 15 | "express-jwt": "^3.4.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /movieanalyst-admin/public/views/includes/navbar.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /movieanalyst-website/public/views/publications.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 |
16 |
17 |

18 |

Our Publication Partners

19 |
20 | 21 |
22 | <% publications.forEach(function(publication, index) { %> 23 |
24 |
25 |
26 |

<%= publication.name %>

27 |
28 |
29 |

30 |
31 |
32 |
33 | <% }) %> 34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /movieanalyst-website/public/views/authors.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 |
16 |
17 |

18 |

Our Movie Critics

19 |
20 | 21 |
22 | <% authors.forEach(function(author, index) { %> 23 |
24 |
25 |
26 |

<%= author.name %>

27 |
28 |
29 | 30 |
31 | 34 |
35 |
36 | <% }) %> 37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /movieanalyst-website/public/views/movies.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 |
16 |
17 |

18 |

Latest Movie Reviews

19 |
20 | 21 |
22 | <% movies.forEach(function(movie, index) { %> 23 |
24 |
25 |
26 |

<%= movie.title %> (<%= movie.release %>)

27 |
28 |
29 |

<%= movie.score %> / 10

30 |
31 | 34 |
35 |
36 | <% }) %> 37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /movieanalyst-admin/public/views/publications.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | <% include ./includes/navbar %> 16 | 17 |
18 |

Editing: Publications

19 | 20 |
21 | <% publications.forEach(function(publication, index) { %> 22 |
23 |
24 |
25 |

Edit: <%= publication.name %>

26 |
27 |
28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 | <% }) %> 36 |
37 | Save 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /movieanalyst-admin/public/views/authors.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | <% include ./includes/navbar %> 16 | 17 |
18 |

Editing: Authors

19 | 20 |
21 | <% authors.forEach(function(author, index) { %> 22 |
23 |
24 |
25 |

Edit: <%= author.name %>

26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | <% }) %> 38 |
39 | Save 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /movieanalyst-admin/public/views/movies.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | <% include ./includes/navbar %> 15 | 16 |
17 |

Editing: Movies

18 | 19 |
20 | <% movies.forEach(function(movie, index) { %> 21 |
22 |
23 |
24 |

Edit: <%= movie.title %>

25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | <% }) %> 41 |
42 | Save 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /movieanalyst-admin/public/views/pending.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | <% include ./includes/navbar %> 16 | 17 |
18 |

Editing: Pending

19 | 20 |
21 | <% movies.forEach(function(movie, index) { %> 22 |
23 |
24 |
25 |

Edit: <%= movie.title %>

26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | <% }) %> 42 |
43 | Save 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /movieanalyst-admin/public/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | <% include ./includes/navbar %> 15 |
16 | 17 |
18 |
19 |
20 |
21 |

Latest Reviews

22 |
23 |
24 |

25 |
26 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |

View Latest Reviews

36 |
37 |
38 |

39 |
40 | 43 |
44 |
45 | 46 |
47 |
48 |
49 |

Our Partners

50 |
51 |
52 |

53 |
54 | 57 |
58 |
59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /movieanalyst-website/public/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 |
16 |
17 |

18 |

Movie Analyst

19 |
20 | 21 |
22 |
23 |
24 |
25 |

Latest Reviews

26 |
27 |
28 |

29 |
30 | 33 |
34 |
35 | 36 |
37 |
38 |
39 |

Authors

40 |
41 |
42 |

43 |
44 | 47 |
48 |
49 | 50 |
51 |
52 |
53 |

Our Partners

54 |
55 |
56 |

57 |
58 | 61 |
62 |
63 |
64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /movieanalyst-admin/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var request = require('superagent'); 3 | 4 | var app = express(); 5 | 6 | app.set('view engine', 'ejs'); 7 | app.set('views', __dirname + '/public/views/'); 8 | 9 | app.use(express.static(__dirname + '/public')); 10 | 11 | var NON_INTERACTIVE_CLIENT_ID = 'YOUR-CLIENT-ID' 12 | var NON_INTERACTIVE_CLIENT_SECRET = 'YOUR-CLIENT-SECRET' 13 | 14 | var authData = { 15 | client_id: NON_INTERACTIVE_CLIENT_ID, 16 | client_secret: NON_INTERACTIVE_CLIENT_SECRET, 17 | grant_type: 'client_credentials', 18 | audience: 'YOUR-API-IDENTIFIER' 19 | } 20 | app.use(getAccessToken); 21 | 22 | // First, authenticate this client and get an access_token 23 | // This could be cached 24 | function getAccessToken(req, res, next){ 25 | request 26 | .post('https://YOUR-AUTH0-DOMAIN.auth0.com/oauth/token') 27 | .send(authData) 28 | .end(function(err, res) { 29 | req.access_token = res.body.access_token 30 | next(); 31 | }) 32 | } 33 | 34 | app.get('/', function(req, res){ 35 | res.render('index'); 36 | }) 37 | 38 | app.get('/movies', getAccessToken, function(req, res){ 39 | request 40 | .get('http://localhost:8080/movies') 41 | .set('Authorization', 'Bearer ' + req.access_token) 42 | .end(function(err, data) { 43 | if(data.status == 403){ 44 | res.send(403, '403 Forbidden'); 45 | } else { 46 | var movies = data.body; 47 | res.render('movies', { movies: movies} ); 48 | } 49 | }) 50 | }) 51 | 52 | app.get('/authors', getAccessToken, function(req, res){ 53 | request 54 | .get('http://localhost:8080/reviewers') 55 | .set('Authorization', 'Bearer ' + req.access_token) 56 | .end(function(err, data) { 57 | if(data.status == 403){ 58 | res.send(403, '403 Forbidden'); 59 | } else { 60 | var authors = data.body; 61 | res.render('authors', {authors : authors}); 62 | } 63 | }) 64 | }) 65 | 66 | app.get('/publications', getAccessToken, function(req, res){ 67 | request 68 | .get('http://localhost:8080/publications') 69 | .set('Authorization', 'Bearer ' + req.access_token) 70 | .end(function(err, data) { 71 | if(data.status == 403){ 72 | res.send(403, '403 Forbidden'); 73 | } else { 74 | var publications = data.body; 75 | res.render('publications', {publications : publications}); 76 | } 77 | }) 78 | }) 79 | 80 | app.get('/pending', getAccessToken, function(req, res){ 81 | request 82 | .get('http://localhost:8080/pending') 83 | .set('Authorization', 'Bearer ' + req.access_token) 84 | .end(function(err, data) { 85 | if(data.status == 403){ 86 | res.send(403, '403 Forbidden'); 87 | } else { 88 | var movies = data.body; 89 | res.render('pending', {movies : movies}); 90 | } 91 | }) 92 | }) 93 | 94 | app.listen(4000); -------------------------------------------------------------------------------- /movieanalyst-website/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var request = require('superagent'); 3 | 4 | var app = express(); 5 | 6 | app.set('view engine', 'ejs'); 7 | app.set('views', __dirname + '/public/views/'); 8 | 9 | app.use(express.static(__dirname + '/public')); 10 | 11 | var NON_INTERACTIVE_CLIENT_ID = 'YOUR-CLIENT-ID' 12 | var NON_INTERACTIVE_CLIENT_SECRET = 'YOUR-CLIENT-SECRET' 13 | 14 | var authData = { 15 | client_id: NON_INTERACTIVE_CLIENT_ID, 16 | client_secret: NON_INTERACTIVE_CLIENT_SECRET, 17 | grant_type: 'client_credentials', 18 | audience: 'YOUR-API-IDENTIFIER' 19 | } 20 | 21 | // First, authenticate this client and get an access_token 22 | // This could be cached 23 | function getAccessToken(req, res, next){ 24 | request 25 | .post('https://YOUR-AUTH0-DOMAIN.auth0.com/oauth/token') 26 | .send(authData) 27 | .end(function(err, res) { 28 | req.access_token = res.body.access_token 29 | next(); 30 | }) 31 | } 32 | 33 | app.get('/', function(req, res){ 34 | res.render('index'); 35 | }) 36 | 37 | app.get('/movies', getAccessToken, function(req, res){ 38 | request 39 | .get('http://localhost:8080/movies') 40 | .set('Authorization', 'Bearer ' + req.access_token) 41 | .end(function(err, data) { 42 | console.log(data); 43 | if(data.status == 403){ 44 | res.send(403, '403 Forbidden'); 45 | } else { 46 | var movies = data.body; 47 | res.render('movies', { movies: movies} ); 48 | } 49 | }) 50 | }) 51 | 52 | app.get('/authors', getAccessToken, function(req, res){ 53 | request 54 | .get('http://localhost:8080/reviewers') 55 | .set('Authorization', 'Bearer ' + req.access_token) 56 | .end(function(err, data) { 57 | if(data.status == 403){ 58 | res.send(403, '403 Forbidden'); 59 | } else { 60 | var authors = data.body; 61 | res.render('authors', {authors : authors}); 62 | } 63 | }) 64 | }) 65 | 66 | app.get('/publications', getAccessToken, function(req, res){ 67 | request 68 | .get('http://localhost:8080/publications') 69 | .set('Authorization', 'Bearer ' + req.access_token) 70 | .end(function(err, data) { 71 | if(data.status == 403){ 72 | res.send(403, '403 Forbidden'); 73 | } else { 74 | var publications = data.body; 75 | res.render('publications', {publications : publications}); 76 | } 77 | }) 78 | }) 79 | 80 | app.get('/pending', getAccessToken, function(req, res){ 81 | request 82 | .get('http://localhost:8080/pending') 83 | .set('Authorization', 'Bearer ' + req.access_token) 84 | .end(function(err, data) { 85 | if(data.status == 403){ 86 | res.send(403, '403 Forbidden'); 87 | } else { 88 | var movies = data.body; 89 | res.render('pending', {movies : movies}); 90 | } 91 | }) 92 | }) 93 | 94 | app.listen(3000); -------------------------------------------------------------------------------- /movieanalyst-api/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var jwt = require('express-jwt'); 4 | var rsaValidation = require('auth0-api-jwt-rsa-validation'); 5 | 6 | console.log(rsaValidation()); 7 | var jwtCheck = jwt({ 8 | secret: rsaValidation(), 9 | algorithms: ['RS256'], 10 | issuer: "https://YOUR-AUTH0-DOMAIN.auth0.com/", 11 | audience: 'YOUR-API-IDENTIFIER' 12 | }); 13 | 14 | var guard = function(req, res, next){ 15 | console.log(req.user); 16 | switch(req.path){ 17 | case '/movies' : { 18 | var permissions = ['general']; 19 | for(var i = 0; i < permissions.length; i++){ 20 | if(req.user.scope.includes(permissions[i])){ 21 | next(); 22 | } else { 23 | res.send(403, {message:'Forbidden'}); 24 | } 25 | } 26 | break; 27 | } 28 | case '/reviewers': { 29 | var permissions = ['general']; 30 | for(var i = 0; i < permissions.length; i++){ 31 | if(req.user.scope.includes(permissions[i])){ 32 | next(); 33 | } else { 34 | res.send(403, {message:'Forbidden'}); 35 | } 36 | } 37 | break; 38 | } 39 | case '/publications': { 40 | var permissions = ['general']; 41 | for(var i = 0; i < permissions.length; i++){ 42 | if(req.user.scope.includes(permissions[i])){ 43 | next(); 44 | } else { 45 | res.send(403, {message:'Forbidden'}); 46 | } 47 | } 48 | break; 49 | } 50 | case '/pending': { 51 | var permissions = ['admin']; 52 | console.log(req.user.scope); 53 | for(var i = 0; i < permissions.length; i++){ 54 | if(req.user.scope.includes(permissions[i])){ 55 | next(); 56 | } else { 57 | res.send(403, {message:'Forbidden'}); 58 | } 59 | } 60 | break; 61 | } 62 | } 63 | }; 64 | app.use(jwtCheck); 65 | app.use(function (err, req, res, next) { 66 | if (err.name === 'UnauthorizedError') { 67 | res.status(401).json({message:'Missing or invalid token'}); 68 | } 69 | }); 70 | app.use(guard); 71 | 72 | 73 | app.get('/movies', function(req, res){ 74 | var movies = [ 75 | {title : 'Suicide Squad', release: '2016', score: 8, reviewer: 'Robert Smith', publication : 'The Daily Reviewer'}, 76 | {title : 'Batman vs. Superman', release : '2016', score: 6, reviewer: 'Chris Harris', publication : 'International Movie Critic'}, 77 | {title : 'Captain America: Civil War', release: '2016', score: 9, reviewer: 'Janet Garcia', publication : 'MoviesNow'}, 78 | {title : 'Deadpool', release: '2016', score: 9, reviewer: 'Andrew West', publication : 'MyNextReview'}, 79 | {title : 'Avengers: Age of Ultron', release : '2015', score: 7, reviewer: 'Mindy Lee', publication: 'Movies n\' Games'}, 80 | {title : 'Ant-Man', release: '2015', score: 8, reviewer: 'Martin Thomas', publication : 'TheOne'}, 81 | {title : 'Guardians of the Galaxy', release : '2014', score: 10, reviewer: 'Anthony Miller', publication : 'ComicBookHero.com'}, 82 | ] 83 | 84 | res.json(movies); 85 | }) 86 | 87 | app.get('/reviewers', function(req, res){ 88 | var authors = [ 89 | {name : 'Robert Smith', publication : 'The Daily Reviewer', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/angelcolberg/128.jpg'}, 90 | {name: 'Chris Harris', publication : 'International Movie Critic', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/bungiwan/128.jpg'}, 91 | {name: 'Janet Garcia', publication : 'MoviesNow', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/grrr_nl/128.jpg'}, 92 | {name: 'Andrew West', publication : 'MyNextReview', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/d00maz/128.jpg'}, 93 | {name: 'Mindy Lee', publication: 'Movies n\' Games', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/laurengray/128.jpg'}, 94 | {name: 'Martin Thomas', publication : 'TheOne', avatar : 'https://s3.amazonaws.com/uifaces/faces/twitter/karsh/128.jpg'}, 95 | {name: 'Anthony Miller', publication : 'ComicBookHero.com', avatar : 'https://s3.amazonaws.com/uifaces/faces/twitter/9lessons/128.jpg'} 96 | ]; 97 | 98 | res.json(authors); 99 | }) 100 | 101 | app.get('/publications', function(req, res){ 102 | var publications = [ 103 | {name : 'The Daily Reviewer', avatar: 'glyphicon-eye-open'}, 104 | {name : 'International Movie Critic', avatar: 'glyphicon-fire'}, 105 | {name : 'MoviesNow', avatar: 'glyphicon-time'}, 106 | {name : 'MyNextReview', avatar: 'glyphicon-record'}, 107 | {name : 'Movies n\' Games', avatar: 'glyphicon-heart-empty'}, 108 | {name : 'TheOne', avatar : 'glyphicon-globe'}, 109 | {name : 'ComicBookHero.com', avatar : 'glyphicon-flash'} 110 | ]; 111 | 112 | res.json(publications); 113 | }) 114 | 115 | app.get('/pending', function(req, res){ 116 | var pending = [ 117 | {title : 'Superman: Homecoming', release: '2017', score: 10, reviewer: 'Chris Harris', publication: 'International Movie Critic'}, 118 | {title : 'Wonder Woman', release: '2017', score: 8, reviewer: 'Martin Thomas', publication : 'TheOne'}, 119 | {title : 'Doctor Strange', release : '2016', score: 7, reviewer: 'Anthony Miller', publication : 'ComicBookHero.com'} 120 | ] 121 | res.json(pending); 122 | }) 123 | 124 | app.listen(8080); --------------------------------------------------------------------------------