34 |
Galleria History Plugin
35 |
Demonstrating a basic history example. Supports most browsers, including FF 3.0+ and IE 7+
36 |
37 |
38 |
39 |
86 |
87 |
Made by Galleria.
88 |
89 |
90 |
99 |
100 |
--------------------------------------------------------------------------------
/public/galleria/themes/fullscreen/fullscreen-demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ').css({
260 | width: 48,
261 | height: 48,
262 | opacity: 0.7,
263 | background:'#000 url('+PATH+'loader.gif) no-repeat 50% 50%'
264 | });
265 |
266 | if ( picasa.length ) {
267 |
268 | // validate the method
269 | if ( typeof Galleria.Picasa.prototype[ picasa[0] ] !== 'function' ) {
270 | Galleria.raise( picasa[0] + ' method not found in Picasa plugin' );
271 | return load.apply( this, args );
272 | }
273 |
274 | // validate the argument
275 | if ( !picasa[1] ) {
276 | Galleria.raise( 'No picasa argument found' );
277 | return load.apply( this, args );
278 | }
279 |
280 | // apply the preloader
281 | window.setTimeout(function() {
282 | self.$( 'target' ).append( loader );
283 | },100);
284 |
285 | // create the instance
286 | p = new Galleria.Picasa();
287 |
288 | // apply Flickr options
289 | if ( typeof self._options.picasaOptions === 'object' ) {
290 | p.setOptions( self._options.picasaOptions );
291 | }
292 |
293 | // call the picasa method and trigger the DATA event
294 | var arg = [];
295 | if ( picasa[0] == 'useralbum' ) {
296 | arg = picasa[1].split('/');
297 | if (arg.length != 2) {
298 | Galleria.raise( 'Picasa useralbum not correctly formatted (should be [user]/[album])');
299 | return;
300 | }
301 | } else {
302 | arg.push( picasa[1] );
303 | }
304 |
305 | arg.push(function(data) {
306 | self._data = data;
307 | loader.remove();
308 | self.trigger( Galleria.DATA );
309 | p.options.complete.call(p, data);
310 | });
311 |
312 | p[ picasa[0] ].apply( p, arg );
313 |
314 | } else {
315 |
316 | // if flickr array not found, pass
317 | load.apply( this, args );
318 | }
319 | };
320 |
321 | }( jQuery ) );
--------------------------------------------------------------------------------
/public/galleria/plugins/flickr/galleria.flickr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Galleria Flickr Plugin 2016-09-03
3 | * http://galleria.io
4 | *
5 | * Copyright (c) 2010 - 2017 worse is better UG
6 | * Licensed under the MIT license
7 | * https://raw.github.com/worseisbetter/galleria/master/LICENSE
8 | *
9 | */
10 |
11 | (function($) {
12 |
13 | /*global jQuery, Galleria, window */
14 |
15 | Galleria.requires(1.25, 'The Flickr Plugin requires Galleria version 1.2.5 or later.');
16 |
17 | // The script path
18 | var PATH = Galleria.utils.getScriptPath();
19 |
20 | /**
21 |
22 | @class
23 | @constructor
24 |
25 | @example var flickr = new Galleria.Flickr();
26 |
27 | @author http://galleria.io
28 |
29 | @requires jQuery
30 | @requires Galleria
31 |
32 | @param {String} [api_key] Flickr API key to be used, defaults to the Galleria key
33 |
34 | @returns Instance
35 | */
36 |
37 | Galleria.Flickr = function( api_key ) {
38 |
39 | this.api_key = api_key || '2a2ce06c15780ebeb0b706650fc890b2';
40 |
41 | this.options = {
42 | max: 30, // photos to return
43 | imageSize: 'medium', // photo size ( thumb,small,medium,big,original )
44 | thumbSize: 'thumb', // thumbnail size ( thumb,small,medium,big,original )
45 | sort: 'interestingness-desc', // sort option ( date-posted-asc, date-posted-desc, date-taken-asc, date-taken-desc, interestingness-desc, interestingness-asc, relevance )
46 | description: false, // set this to true to get description as caption
47 | complete: function(){}, // callback to be called inside the Galleria.prototype.load
48 | backlink: false // set this to true if you want to pass a link back to the original image
49 | };
50 | };
51 |
52 | Galleria.Flickr.prototype = {
53 |
54 | // bring back the constructor reference
55 |
56 | constructor: Galleria.Flickr,
57 |
58 | /**
59 | Search for anything at Flickr
60 |
61 | @param {String} phrase The string to search for
62 | @param {Function} [callback] The callback to be called when the data is ready
63 |
64 | @returns Instance
65 | */
66 |
67 | search: function( phrase, callback ) {
68 | return this._find({
69 | text: phrase
70 | }, callback );
71 | },
72 |
73 | /**
74 | Search for anything at Flickr by tag
75 |
76 | @param {String} tag The tag(s) to search for
77 | @param {Function} [callback] The callback to be called when the data is ready
78 |
79 | @returns Instance
80 | */
81 |
82 | tags: function( tag, callback ) {
83 | return this._find({
84 | tags: tag
85 | }, callback);
86 | },
87 |
88 | /**
89 | Get a user's public photos
90 |
91 | @param {String} username The username as shown in the URL to fetch
92 | @param {Function} [callback] The callback to be called when the data is ready
93 |
94 | @returns Instance
95 | */
96 |
97 | user: function( username, callback ) {
98 | return this._call({
99 | method: 'flickr.urls.lookupUser',
100 | url: 'flickr.com/photos/' + username
101 | }, function( data ) {
102 | this._find({
103 | user_id: data.user.id,
104 | method: 'flickr.people.getPublicPhotos'
105 | }, callback);
106 | });
107 | },
108 |
109 | /**
110 | Get photos from a photoset by ID
111 |
112 | @param {String|Number} photoset_id The photoset id to fetch
113 | @param {Function} [callback] The callback to be called when the data is ready
114 |
115 | @returns Instance
116 | */
117 |
118 | set: function( photoset_id, callback ) {
119 | return this._find({
120 | photoset_id: photoset_id,
121 | method: 'flickr.photosets.getPhotos'
122 | }, callback);
123 | },
124 |
125 | /**
126 | Get photos from a gallery by ID
127 |
128 | @param {String|Number} gallery_id The gallery id to fetch
129 | @param {Function} [callback] The callback to be called when the data is ready
130 |
131 | @returns Instance
132 | */
133 |
134 | gallery: function( gallery_id, callback ) {
135 | return this._find({
136 | gallery_id: gallery_id,
137 | method: 'flickr.galleries.getPhotos'
138 | }, callback);
139 | },
140 |
141 | /**
142 | Search groups and fetch photos from the first group found
143 | Useful if you know the exact name of a group and want to show the groups photos.
144 |
145 | @param {String} group The group name to search for
146 | @param {Function} [callback] The callback to be called when the data is ready
147 |
148 | @returns Instance
149 | */
150 |
151 | groupsearch: function( group, callback ) {
152 | return this._call({
153 | text: group,
154 | method: 'flickr.groups.search'
155 | }, function( data ) {
156 | this.group( data.groups.group[0].nsid, callback );
157 | });
158 | },
159 |
160 | /**
161 | Get photos from a group by ID
162 |
163 | @param {String} group_id The group id to fetch
164 | @param {Function} [callback] The callback to be called when the data is ready
165 |
166 | @returns Instance
167 | */
168 |
169 | group: function ( group_id, callback ) {
170 | return this._find({
171 | group_id: group_id,
172 | method: 'flickr.groups.pools.getPhotos'
173 | }, callback );
174 | },
175 |
176 | /**
177 | Set flickr options
178 |
179 | @param {Object} options The options object to blend
180 |
181 | @returns Instance
182 | */
183 |
184 | setOptions: function( options ) {
185 | $.extend(this.options, options);
186 | return this;
187 | },
188 |
189 |
190 | // call Flickr and raise errors
191 |
192 | _call: function( params, callback ) {
193 |
194 | var url = 'https://api.flickr.com/services/rest/?';
195 |
196 | var scope = this;
197 |
198 | params = $.extend({
199 | format : 'json',
200 | jsoncallback : '?',
201 | api_key: this.api_key
202 | }, params );
203 |
204 | $.each(params, function( key, value ) {
205 | url += '&' + key + '=' + value;
206 | });
207 |
208 | $.getJSON(url, function(data) {
209 | if ( data.stat === 'ok' ) {
210 | callback.call(scope, data);
211 | } else {
212 | Galleria.raise( data.code.toString() + ' ' + data.stat + ': ' + data.message, true );
213 | }
214 | });
215 | return scope;
216 | },
217 |
218 |
219 | // "hidden" way of getting a big image (~1024) from flickr
220 |
221 | _getBig: function( photo ) {
222 |
223 | if ( photo.url_l ) {
224 | return photo.url_l;
225 | } else if ( parseInt( photo.width_o, 10 ) > 1280 ) {
226 |
227 | return 'https://farm'+photo.farm + '.static.flickr.com/'+photo.server +
228 | '/' + photo.id + '_' + photo.secret + '_b.jpg';
229 | }
230 |
231 | return photo.url_o || photo.url_z || photo.url_m;
232 |
233 | },
234 |
235 |
236 | // get image size by option name
237 |
238 | _getSize: function( photo, size ) {
239 |
240 | var img;
241 |
242 | switch(size) {
243 |
244 | case 'thumb':
245 | img = photo.url_t;
246 | break;
247 |
248 | case 'small':
249 | img = photo.url_s;
250 | break;
251 |
252 | case 'big':
253 | img = this._getBig( photo );
254 | break;
255 |
256 | case 'original':
257 | img = photo.url_o ? photo.url_o : this._getBig( photo );
258 | break;
259 |
260 | default:
261 | img = photo.url_z || photo.url_m;
262 | break;
263 | }
264 | return img;
265 | },
266 |
267 |
268 | // ask flickr for photos, parse the result and call the callback with the galleria-ready data array
269 |
270 | _find: function( params, callback ) {
271 |
272 | params = $.extend({
273 | method: 'flickr.photos.search',
274 | extras: 'url_t,url_m,url_o,url_s,url_l,url_z,description',
275 | sort: this.options.sort,
276 | per_page: Math.min( this.options.max, 500 )
277 | }, params );
278 |
279 | return this._call( params, function(data) {
280 |
281 | var gallery = [],
282 | photos = data.photos ? data.photos.photo : data.photoset.photo,
283 | len = photos.length,
284 | photo,
285 | i;
286 |
287 | for ( i=0; i
').css({
335 | width: 48,
336 | height: 48,
337 | opacity: 0.7,
338 | background:'#000 url('+PATH+'loader.gif) no-repeat 50% 50%'
339 | });
340 |
341 | if ( flickr.length ) {
342 |
343 | // validate the method
344 | if ( typeof Galleria.Flickr.prototype[ flickr[0] ] !== 'function' ) {
345 | Galleria.raise( flickr[0] + ' method not found in Flickr plugin' );
346 | return load.apply( this, args );
347 | }
348 |
349 | // validate the argument
350 | if ( !flickr[1] ) {
351 | Galleria.raise( 'No flickr argument found' );
352 | return load.apply( this, args );
353 | }
354 |
355 | // apply the preloader
356 | window.setTimeout(function() {
357 | self.$( 'target' ).append( loader );
358 | },100);
359 |
360 | // create the instance
361 | f = new Galleria.Flickr();
362 |
363 | // apply Flickr options
364 | if ( typeof self._options.flickrOptions === 'object' ) {
365 | f.setOptions( self._options.flickrOptions );
366 | }
367 |
368 | // call the flickr method and trigger the DATA event
369 | f[ flickr[0] ]( flickr[1], function( data ) {
370 |
371 | self._data = data;
372 | loader.remove();
373 | self.trigger( Galleria.DATA );
374 | f.options.complete.call(f, data);
375 |
376 | });
377 | } else {
378 |
379 | // if flickr array not found, pass
380 | load.apply( this, args );
381 | }
382 | };
383 |
384 | }( jQuery ) );
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Writing a photo gallery Web Service from scratch with Node.JS and Dropbox with production deployment on Heroku
2 | *by Ruben Rincon*
3 | Nov 2017
4 |
5 | In this step by step tutorial we will build a production-ready photo gallery Web Service using Node.JS and Dropbox. It will show you the artifacts needed to implement a Web Service (which we will also call middleware) that authenticates a user via OAuth with Dropbox to read a user folder, nicely display images in a photo gallery and deploy the code on Heroku for production.
6 |
7 | These are the main technologies and concepts we will cover:
8 |
9 |
10 | - Node.JS
11 | - Express
12 | - Front end and back end
13 | - Dropbox API
14 | - OAuth authentication
15 | - Cookies and sessions
16 | - Heroku
17 | - Heroku addins
18 | - Security on Node.JS
19 |
20 |
21 | The Web Service will allow users to nicely display on a gallery images stored in a Dropbox folder of their own account. To do this, users will first authenticate to Dropbox via OAuth, then the middleware will fetch a number of images from a specific folder and render them using a JavaScript library called Galleria.
22 |
23 | Dropbox API has two types of permissions, Full Dropbox access or Folder access. For the sake of security (and your trust), we are only using the second (Folder access), so the middleware is only allowed to read/write to a specific folder on the users Dropbox account and nothing else.
24 |
25 |
26 | # 1. Node.JS and Express
27 |
28 | The great thing with Node is that you can write server code using JavaScript. Nonetheless, the Web Service needs some structure and a separation of the back end and the front end code as well as any public resource. Express will help exactly with that.
29 |
30 | To install Node.JS, you can simply go to [Nodejs.org](https://nodejs.org/en/) and get the latest version. For this tutorial, we need support for certain elements of EcmaScript7 (ES7) so I recommend the 8.2.1 version or higher if exists when you read this.
31 |
32 | After Node is installed in your local environment, we will create a project structure using Express generator, but before that, we need to pick a template engine. Express uses different types of engines to generate HTML files on the fly before sending them as part of the response, this allows to create HTML content dynamically on each call to the server. In this tutorial, we will use [Handlebars](http://handlebarsjs.com/), a simplistic template engine that leverages actual HTML instead of developing its own template language.
33 |
34 | First install express generator run
35 |
36 |
37 | npm install express-generator -g
38 |
39 |
40 | Now create a project called *dbximgs* using handlebars template engine
41 |
42 |
43 | express --hbs dbximgs
44 |
45 |
46 | This creates a folder called dbximgs with the structure below. Notice, the dependencies of the project are not yet installed, only a file structure has been created.
47 |
48 | 
49 |
50 |
51 | These are the main items to highlight on that structure
52 |
53 | - **package.json** lists the dependencies to be installed and general info on the project. Also declares the entry point of the app, which is the bin/www file.
54 | - **bin/www** is the file that declares the port and actually gets the server running. You won’t need to modify it in this sample.
55 | - **app.js** middleware libraries get initialized on this file. Think about middleware libraries like code that gets raw texts requests from the Web, transforms them and then formats them nicely.
56 | - **routes/index.js** this code gets executed when you hit an endpoint on the server.
57 | - **public folder:** is the front end or resources and files that will eventually find their way to the user. Never store any critical code here, as it could be accessed by users via their browser, that is what the back end is for.
58 |
59 |
60 | At this point, you can get your minimal Web Service running locally. First you actually need to install the dependencies declared on your package.json using the `npm install` command
61 |
62 |
63 | cd dbximgs
64 | npm install
65 |
66 |
67 | Once the installation is complete, run
68 |
69 |
70 | npm start
71 |
72 |
73 | Go to your browser and enter [http://localhost:3000](http://localhost:3000) on the address bar and you will see your server running:
74 |
75 |
76 | 
77 |
78 | # 2. Front end and back end
79 |
80 | Before continuing with the code, it is important to set some additional structure to our project to have a clean separation of responsibilities.
81 |
82 | **Back end**
83 | In general, anything that is not in the public folder constitutes the back end. Whenever you make a change to the back end, you need to restart the server to see it. The main files that we will modify are:
84 |
85 |
86 | - **app.js** to setup the middleware or code that handles the requests before they reach our router.
87 | - **routes/index.js** will contain all the endpoints of our server.
88 |
89 | Additionally, create the following two files for the back end at the root folder of the project.
90 |
91 | - **config.js** will contain all the configuration variables, paths, secrets, etc.
92 | - **controller.js** will contain all the business logic and implementation of the routes.
93 |
94 | **Front end**
95 | Defined as anything that lives in the **public** folder. The user and the user’s browser will interact with and it is normally HTML, CSS, JavaScript and images contained there. You don’t need to restart the server to see changes on it.
96 |
97 |
98 | ## A bit of front end work with Galleria
99 |
100 | We will be using a JavaScript library called [Galleria](https://galleria.io/). This library uses jQuery and will allow us to add images on the fly and display them nicely.
101 |
102 | We will keep the front end development as simple as possible as the main topic of the tutorial is the back end end part. So let us set a page that properly renders Galleria.
103 |
104 | First download the Galleria library [from this link](https://galleria.io/get-started/) and uncompress it.
105 |
106 | Now copy the galleria folder inside the **/public** folder and it should look like this
107 |
108 |
109 | 
110 |
111 |
112 |
113 | Our first exercise is to put a simple template page running using handlebars.
114 | If you want to know more about how to use handlebars, [here is a good resource](https://webapplog.com/handlebars/).
115 |
116 | A template generates HTML code on the fly before it gets sent to the client. Our first exercise will receive a request on the home endpoint `/` , package the path of several images on an array and pass it to a handlebars template.
117 |
118 |
119 | 1. First put any three images in the **/public/images** folder, you can call them *a**.jpg*, *b**.jpg* and *c**.jpg* for simplicity.
120 | 2. Add the route: in the **routes/index.js** file, replace the *router.get* method with the following code.
121 |
122 |
123 | **routes/index.js**
124 |
125 | ```javascript
126 | //first add the reference to the controller
127 | var controller = require('../controller');
128 |
129 | /* GET home page. */
130 | router.get('/', controller.home);
131 | ```
132 |
133 | 3. Add the controller: add the implementation of the endpoint in the controller. If you haven’t, create a **controller.js** file at the root level and add the following code.
134 |
135 | **controller.js**
136 |
137 | ```javascript
138 | module.exports.home = (req,res,next)=>{
139 | var paths = ['images/a.jpg','images/b.jpg','images/c.jpg'];
140 | res.render('gallery', { imgs: paths, layout:false});
141 | };
142 | ```
143 |
144 |
145 | The `res.render` gets two arguments: the first is the name of the template (which we have not created yet) and the second is a JSON object that will be passed to the template engine. In this case, we will pass our paths array as imgs and the layout:false will ensure that handlebars doesn’t use the template layout.
146 |
147 | Now create the template /views/gallery.hbs and copy this code
148 |
149 | **/views/gallery.hbs**
150 |
151 | ```html
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 | {% raw %}
163 | {{#each imgs}}
164 |

165 | {{/each}}
166 | {% endraw %}
167 |
168 |
169 |
170 | ```
171 |
172 | You can see in the body part that we iterate through the `imgs` object passed creating HTML code with an image tag per array element. Let’s now add the css and JavaScript file referenced in the header above.
173 |
174 | **public/javascripts/page.js**
175 |
176 | ```javascript
177 | jQuery(document).ready(function(){
178 | Galleria.loadTheme('/galleria/themes/classic/galleria.classic.min.js');
179 | Galleria.run('.galleria');
180 | });
181 | ```
182 |
183 | **public/stylesheets/page.css**
184 |
185 | ```css
186 | .galleria{
187 | max-width: 100%;
188 | height: 700px;
189 | margin: 0 auto;
190 | }
191 | ```
192 |
193 |
194 | 🎯 The source code at this point can be found in [this link](https://github.com/dropbox/dbximages/tree/frontendonly)
195 |
196 |
197 | Now let us run the server with
198 |
199 | npm start
200 |
201 | And in your browser navigate to [http://localhost:3000](http://localhost:3000/gallery) and it will look like this
202 |
203 |
204 | 
205 |
206 |
207 | Now that we have our front end running. Let us do the back end part, which is the main focus of this tutorial.
208 |
209 |
210 | # 3. Dropbox app
211 |
212 | We want to access the Dropbox folder of a user who authorizes the middleware to read it and populate a gallery.
213 |
214 | To do this, we will first need to create a Dropbox app. For that you need a Dropbox account. If you don’t have one, create one first and then go to [https://www.dropbox.com/developers/apps](https://www.dropbox.com/developers/apps)
215 | And click on **Create App**
216 |
217 | Then choose Dropbox API, App Folder, put a name on your app and click on **Create App.**
218 |
219 |
220 | 
221 |
222 |
223 | We chose **App folder** permission so the middleware can only read and write to a single folder to those users who authorize the app.
224 |
225 | After this, you want to also enable additional users in this app, otherwise only you can use it. In the settings page of your app you will find a button to do this.
226 |
227 |
228 | 
229 |
230 |
231 |
232 |
233 | # 4. OAuth with authorization code grant flow
234 |
235 |
236 | This application should be able to read a specific app folder for any Dropbox user who consents. For this we need to build an authentication flow where user is redirected to Dropbox to enter credentials and then authorize this app to read users Dropbox. After this is done, a folder inside Dropbox will be created with the name of this app and the middleware will be able to access the contents of that folder only.
237 |
238 | The most secure way to do this is using an ***authorization code flow.*** In this flow, after the authorization step, Dropbox issues a code to the middleware that is exchanged for a token. The middleware stores the token and is never visible to the Web browser. To know who is the user requesting the token, we use a session. At first, we will simply use a hardcoded session value and save it in a cache, but later we will replace it with a proper library to manage sessions and cookies and will be stored on a persistent database.
239 |
240 | Before writing any code, we need to do an important configuration step in Dropbox:
241 |
242 | Pre-register a redirect URL in the Dropbox admin console. Temporarily we will use a localhost endpoint which is the only permitted http URL. For anything different to home, you need to use https. We will use a `/oauthredirect` endpoint. So enter the URL [http://localhost:3000/](http://localhost:3000/dbxlogin)oauthredirect and press the **Add** button.
243 |
244 | Also we we will not use implicit grant, so you can disable it.
245 |
246 |
247 | 
248 |
249 |
250 | 💡 If you are interested in learning more about OAuth, this is a [good read](https://spring.io/blog/2011/11/30/cross-site-request-forgery-and-oauth2) 👍
251 |
252 | The whole authorization flow will have all the following steps which I will explain right after.
253 |
254 |
255 | 
256 |
257 | 1. When a user calls the *home* `/` endpoint, the middleware retrieves a session for that specific user. (we will first use a hardcoded value and a cache, but later we will use a proper library).
258 | 2. The middleware checks if there is an OAuth token already issued for that session. In this case, it does not find any, so an authorization sequence is started.
259 | 3. The middleware redirects to a `/login` endpoint which will construct a URL to perform the authentication via Dropbox.
260 | 4. Part of this URL is a state, which is a set of characters that will be passed to Dropbox and will be visible in the browser address bar as part of the URL.
261 | 5. The state is saved along the session.
262 | 6. User is redirected to the authentication server in Dropbox along with the state and a URL to redirect back to the middleware, in this case we will use the `/oauthredirect` endpoint.
263 | 7. The user will authenticate to Dropbox and authorize this application to read a specific folder.
264 | 8. Dropbox will redirect the user to the `/oauthredirect` endpoint of the middleware and will pass a code and give us back the state we passed.
265 | 9. The middleware validates that there is an existing session for that state, this is a protection against CSRF attacks.
266 | 10. The middleware makes a POST call to Dropbox to exchange the code for an OAuth token. In this call, the middleware will pass the application key/secret to Dropbox.
267 | 11. The middleware will save the token along with the session.
268 | 12. User will be redirected to home `/` but now the user will have a token to make calls to Dropbox API.
269 |
270 | Let us write all the code now… 👨💻
271 |
272 | First we need a number of configuration items in the **config.js** file at the root level. You will need to replace the appkey/secret from your own Dropbox console.
273 |
274 | **config.js**
275 |
276 | ```javascript
277 | module.exports = {
278 | DBX_API_DOMAIN: 'https://api.dropboxapi.com',
279 | DBX_OAUTH_DOMAIN: 'https://www.dropbox.com',
280 | DBX_OAUTH_PATH: '/oauth2/authorize',
281 | DBX_TOKEN_PATH: '/oauth2/token',
282 | DBX_APP_KEY:'',
283 | DBX_APP_SECRET:'',
284 | OAUTH_REDIRECT_URL:"http://localhost:3000/oauthredirect",
285 | }
286 | ```
287 |
288 |
289 | 🛑 ⚠️ If you are using a version control system such as git at this point, remember the Dropbox key/secret will be hard coded in some version of your code, which is especially bad if you are storing it on a public repository. If that is the case, consider using **dotenv** library along with the .**gitignore** file explained on section 7.
290 |
291 |
292 | Now let’s add the business logic. To create a random state we will use the crypto library (which is part of Node) and to temporarily store it in a cache, we will use node-cache library. The node-cache simply receives a key/value pair and an expire it in a number of seconds. We will arbitrarily set it to 10 mins = 600 seconds.
293 |
294 | Let us first install the node-cache library
295 |
296 |
297 | npm install node-cache --save
298 |
299 |
300 | 💡 The `--save` adds a dependency in the package.json file.
301 |
302 | For the steps 1,2 and 3 in the flow above, modify the *home* method in the **controller**.**js** If there is no token, we redirect to the `/login` endpoint passing a temporary session in the query. Remember we will change this for a session library later.
303 |
304 | **controller.js**
305 |
306 | ```javascript
307 | const
308 | crypto = require('crypto'),
309 | config = require('./config'),
310 | NodeCache = require( "node-cache" );
311 | var mycache = new NodeCache();
312 |
313 | //steps 1,2,3
314 | module.exports.home = (req,res,next)=>{
315 | let token = mycache.get("aTempTokenKey");
316 | if(token){
317 | let paths = ['images/a.jpg','images/b.jpg','images/c.jpg'];
318 | res.render('gallery', { imgs: paths });
319 | }else{
320 | res.redirect('/login');
321 | }
322 | }
323 |
324 | //steps 4,5,6
325 | module.exports.login = (req,res,next)=>{
326 |
327 | //create a random state value
328 | let state = crypto.randomBytes(16).toString('hex');
329 |
330 | //Save state and temporarysession for 10 mins
331 | mycache.set(state, "aTempSessionValue", 600);
332 |
333 | let dbxRedirect= config.DBX_OAUTH_DOMAIN
334 | + config.DBX_OAUTH_PATH
335 | + "?response_type=code&client_id="+config.DBX_APP_KEY
336 | + "&redirect_uri="+config.OAUTH_REDIRECT_URL
337 | + "&state="+state;
338 |
339 | res.redirect(dbxRedirect);
340 | }
341 | ```
342 |
343 | Now we need to list the login endpoint in the **routes/index.js** , so add the following line.
344 |
345 | **routes/index.js**
346 |
347 | router.get('/login', controller.login);
348 |
349 | At this point, you can test it again by running
350 |
351 |
352 | npm start
353 |
354 | and hitting http://localhost:3000 should forward to an authentication/authorization page like this
355 |
356 |
357 | 
358 |
359 |
360 | Once you authorize, you will see an error as we have not added an endpoint to be redirected back, but take a look at the url, you will see there the **state** you sent and the **code** from Dropbox that you will use to get a token .
361 |
362 |
363 | 
364 |
365 | ## Exchanging code for token
366 |
367 | When Dropbox redirects to the middleware, there are two possible outcomes:
368 |
369 | - A successful call will include a **code** and a **state**
370 | - An error call will include an **error_description** query parameter
371 |
372 | In the success case, we will exchange the code by a token via a POST call to the **/oauth2/token** call in Dropbox. To make that call we will use the **request-promise** library, which is a wrapper to the **request** library adding promise capabilities on top of it.
373 |
374 | Let us first install the request-promise and request libraries with the following command
375 |
376 |
377 | npm install request request-promise --save
378 |
379 | Now add one more method to the controller with the logic to exchange the code via the Dropbox API. Once the token is obtained we will temporarily save it on cache and redirect to the home path.
380 |
381 | **controller.js**
382 |
383 | ```javascript
384 | //add to the variable definition section on the top
385 | rp = require('request-promise');
386 |
387 | //steps 8-12
388 | module.exports.oauthredirect = async (req,res,next)=>{
389 |
390 | if(req.query.error_description){
391 | return next( new Error(req.query.error_description));
392 | }
393 |
394 | let state= req.query.state;
395 | if(!mycache.get(state)){
396 | return next(new Error("session expired or invalid state"));
397 | }
398 |
399 | //Exchange code for token
400 | if(req.query.code ){
401 |
402 | let options={
403 | url: config.DBX_API_DOMAIN + config.DBX_TOKEN_PATH,
404 | //build query string
405 | qs: {'code': req.query.code,
406 | 'grant_type': 'authorization_code',
407 | 'client_id': config.DBX_APP_KEY,
408 | 'client_secret':config.DBX_APP_SECRET,
409 | 'redirect_uri':config.OAUTH_REDIRECT_URL},
410 | method: 'POST',
411 | json: true }
412 |
413 | try{
414 |
415 | let response = await rp(options);
416 |
417 | //we will replace later cache with a proper storage
418 | mycache.set("aTempTokenKey", response.access_token, 3600);
419 | res.redirect("/");
420 |
421 | }catch(error){
422 | return next(new Error('error getting token. '+error.message));
423 | }
424 | }
425 | }
426 | ```
427 |
428 | The beauty of using the **request-promise** library and ES7 **async await** is that we can write our code as if it was all synchronous while this code will not actually block the server. The **await** indicator will simply yield until the `rp(options)` call has a returned a value (or error) and then it will be picked up again. Notice that the function has to be marked **async** for this to work. If the promise fails, it will be captured by the catch and we pass it to the app to handle it, so it is pretty safe.
429 |
430 | 💡 If you have any questions on how the options for the request are formed, you can check the [request](https://www.npmjs.com/package/request) documentation.
431 | 💡 If you wan to know more about async await this is a [good source](https://strongloop.com/strongblog/async-error-handling-expressjs-es7-promises-generators/)
432 |
433 | Now we need to hook the route to the controller in the **routes/index.js** file.
434 |
435 | **routes/index.js**
436 |
437 | router.get('/oauthredirect',controller.oauthredirect);
438 |
439 | and run the server again with `npm start` and try again http://localhost:3000
440 | You should see again the gallery with the mock images displaying correctly.
441 |
442 |
443 |
444 | # 5. Fetching images from Dropbox
445 |
446 | Now that we are able to see a gallery of images. We want to read the images from Dropbox.
447 | After the user authorizes the application to read a folder in Dropbox, a folder will be created within the ***Apps*** folder with the name of this app, in this case ***dbximgs demo***. If the ***Apps*** folder didn’t exist before, it will be created. So go ahead and populate that folder with some images you want. For security purposes we will use temporary links that are valid only for 4 hours.
448 |
449 | Now we need to make a call to the Dropbox API to fetch temporary links for those images. We will follow these steps:
450 |
451 | 1. Call Dropbox `/list_folder` endpoint which returns information about the files contained in the App/dbximgs demo
452 | 2. Filter the response to images only, ignoring other types of files and folders
453 | 3. Grab only the `path_lower` field from those results
454 | 4. For each `path_lower` call the `get_temporary_link` endpoint, this link is valid for 4 hours.
455 | 5. Grab the `link` field of the response
456 | 6. Pass all the temporary links to the gallery
457 |
458 | 💡 More information about these endpoints in the [Dropbox documentation](https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link)
459 |
460 | First, you need to add a couple configuration fields
461 |
462 | **config.js**
463 |
464 | ```javascript
465 | DBX_LIST_FOLDER_PATH:'/2/files/list_folder',
466 | DBX_LIST_FOLDER_CONTINUE_PATH:'/2/files/list_folder/continue',
467 | DBX_GET_TEMPORARY_LINK_PATH:'/2/files/get_temporary_link',
468 | ```
469 |
470 | This is the code you need to add to controllers.js
471 |
472 | **controller.js**
473 |
474 | ```javascript
475 | /*Gets temporary links for a set of files in the root folder of the app
476 | It is a two step process:
477 | 1. Get a list of all the paths of files in the folder
478 | 2. Fetch a temporary link for each file in the folder */
479 | async function getLinksAsync(token){
480 |
481 | //List images from the root of the app folder
482 | let result= await listImagePathsAsync(token,'');
483 |
484 | //Get a temporary link for each of those paths returned
485 | let temporaryLinkResults= await getTemporaryLinksForPathsAsync(token,result.paths);
486 |
487 | //Construct a new array only with the link field
488 | var temporaryLinks = temporaryLinkResults.map(function (entry) {
489 | return entry.link;
490 | });
491 |
492 | return temporaryLinks;
493 | }
494 |
495 |
496 | /*
497 | Returns an object containing an array with the path_lower of each
498 | image file and if more files a cursor to continue */
499 | async function listImagePathsAsync(token,path){
500 |
501 | let options={
502 | url: config.DBX_API_DOMAIN + config.DBX_LIST_FOLDER_PATH,
503 | headers:{"Authorization":"Bearer "+token},
504 | method: 'POST',
505 | json: true ,
506 | body: {"path":path}
507 | }
508 |
509 | try{
510 | //Make request to Dropbox to get list of files
511 | let result = await rp(options);
512 |
513 | //Filter response to images only
514 | let entriesFiltered= result.entries.filter(function(entry){
515 | return entry.path_lower.search(/\.(gif|jpg|jpeg|tiff|png)$/i) > -1;
516 | });
517 |
518 | //Get an array from the entries with only the path_lower fields
519 | var paths = entriesFiltered.map(function (entry) {
520 | return entry.path_lower;
521 | });
522 |
523 | //return a cursor only if there are more files in the current folder
524 | let response= {};
525 | response.paths= paths;
526 | if(result.hasmore) response.cursor= result.cursor;
527 | return response;
528 |
529 | }catch(error){
530 | return next(new Error('error listing folder. '+error.message));
531 | }
532 | }
533 |
534 |
535 | //Returns an array with temporary links from an array with file paths
536 | function getTemporaryLinksForPathsAsync(token,paths){
537 |
538 | var promises = [];
539 | let options={
540 | url: config.DBX_API_DOMAIN + config.DBX_GET_TEMPORARY_LINK_PATH,
541 | headers:{"Authorization":"Bearer "+token},
542 | method: 'POST',
543 | json: true
544 | }
545 |
546 | //Create a promise for each path and push it to an array of promises
547 | paths.forEach((path_lower)=>{
548 | options.body = {"path":path_lower};
549 | promises.push(rp(options));
550 | });
551 |
552 | //returns a promise that fullfills once all the promises in the array complete or one fails
553 | return Promise.all(promises);
554 | }
555 | ```
556 |
557 |
558 | Finally, modify again the home method in the **controller.js** to look like the code below. First of all, you will notice we added an async modifier as we use an await call to get the links from Dropbox from the code above.
559 |
560 | **controller.js** .home method
561 |
562 | ```javascript
563 | //steps 1,2,3
564 | module.exports.home = async (req,res,next)=>{
565 | let token = mycache.get("aTempTokenKey");
566 | if(token){
567 | try{
568 | let paths = await getLinksAsync(token);
569 | res.render('gallery', { imgs: paths, layout:false});
570 | }catch(error){
571 | return next(new Error("Error getting images from Dropbox"));
572 | }
573 | }else{
574 | res.redirect('/login');
575 | }
576 | }
577 | ```
578 |
579 | You can run the server and test it. You should be able to see the images from the folder in your gallery after login into Dropbox.
580 |
581 | 👁️ Make sure you have images in the folder created after you login to Dropbox and authorize the application.
582 |
583 |
584 | 
585 |
586 |
587 | 🎯 The source code at this point can be found in [this link](https://github.com/dropbox/dbximages/tree/backendnosession)
588 |
589 |
590 | # 6. Cookies, sessions and Redis database
591 |
592 | Until now, we use a hardcoded session in the `/login` endpoint. We are going to make several changes for the sake of security and it comes with three Web dev components: cookies, sessions and a session store.
593 |
594 | **Cookies:** data stored as plain text on the users browser. In our case it will be a sessionID.
595 | **Session**: set of data that contains current status of a user as well as a token to access Dropbox resources. Identified via sessionID.
596 | **Session store:** where sessions are stored. We use Redis as this is a fast, lean and popular key value storage.
597 |
598 | Our new flow will be something like this:
599 |
600 | 1. When a user hits our main server endpoint `/` for the first time, a session gets automatically created and a sessionID gets stored via cookies in the browser. We check if the session has a current token.
601 | 2. If there is no token, we redirect to the `login` endpoint, we create a random state value and store the sessionID in cache with the state as key.
602 | 3. When we get redirected back, we find in the cache a SessionID for that state and compare against the current sessionID. This indicates we originated that authentication flow.
603 | 4. When the OAuth flow is complete, we regenerate the session (creating a new sessionID) and store the token as part of the new session.
604 | 5. We redirect back to our main endpoint `/`.
605 | 6. As a token is found in the current session, the gallery data is returned using the token.
606 |
607 | **Installing redis**
608 | To test this locally, you need to install Redis in your machine which can be obtained [here](https://redis.io/download)
609 |
610 | Once you unpack redis on your local machine, just go to the redis folder and run.
611 |
612 |
613 | src/redis-server
614 |
615 | You don’t need to worry about configuration as this is only a local test instance, the production one will be using Heroku. When it runs, it will look like this:
616 |
617 | 
618 |
619 |
620 | We will also need the following Node libraries
621 |
622 | [**express-session**](https://www.npmjs.com/package/express-session)**:** Node library to manage sessions
623 | [**express-sessions**](https://www.npmjs.com/package/express-sessions)**:** Node library that wraps the session store
624 | [**redis**](https://www.npmjs.com/package/redis)**:** Node library to manipulate redis
625 |
626 | so run the following commands:
627 |
628 | npm install express-sessions express-session redis --save
629 |
630 | You need to add a sessionID secret in the config file (This is the secret used to sign the session ID cookie). Pick your own.
631 |
632 | **config.js**
633 |
634 | SESSION_ID_SECRET:'cAt2-D0g-cOW',
635 |
636 | And now initialize the libraries in the app.js file where any middleware gets configured with the following configuration. Simply add the code below right after the initialization of the **app** variable. `var app = express();`
637 |
638 | **app.js**
639 |
640 | ```javascript
641 | var config = require('./config');
642 | var redis = require('redis');
643 | var client = redis.createClient();
644 | var crypto = require('crypto');
645 | var session = require('express-session');
646 |
647 | //initialize session
648 | var sess = {
649 | secret: config.SESSION_ID_SECRET,
650 | cookie: {}, //add empty cookie to the session by default
651 | resave: false,
652 | saveUninitialized: true,
653 | genid: (req) => {
654 | return crypto.randomBytes(16).toString('hex');;
655 | },
656 | store: new (require('express-sessions'))({
657 | storage: 'redis',
658 | instance: client, // optional
659 | collection: 'sessions' // optional
660 | })
661 | }
662 | app.use(session(sess));
663 | ```
664 |
665 | Finally, we will make 5 changes in the controller: 1 in the login method, 1 in the home method, 2 in the oauthredirect method and we will also add a new method to regenerate a session.
666 |
667 |
668 | 1. We save now in the cache the sessionID instead of a hardcoded value.
669 |
670 | **controller.js login method**
671 |
672 | ```javascript
673 | // mycache.set(state, "aTempSessionValue", 600);
674 | mycache.set(state, req.sessionID, 600);
675 | ```
676 |
677 |
678 | 2. Instead of reading the token from cache, read it from the session.
679 |
680 | **controller.js home method**
681 |
682 | ```javascript
683 | //let token = mycache.get("aTempTokenKey");
684 | let token = req.session.token;
685 | ```
686 |
687 | 3. In the oauthredirect, now we actually make sure that the state value we have just received from Dropbox is the same we previously stored.
688 |
689 | **controller.js** oauthredirect **method**
690 |
691 | ```javascript
692 | //if(!mycache.get(state)){
693 | if(mycache.get(state)!=req.sessionID){
694 | ```
695 |
696 | 4. For security reasons, whenever we get a new token, we regenerate the session and then we save it. Let us use a method called regenerateSessionAsync that receives the request.
697 |
698 | **controller.js** oauthredirect **method**
699 |
700 | ```javascript
701 | //mycache.set("aTempTokenKey", response.access_token, 3600);
702 | await regenerateSessionAsync(req);
703 | req.session.token = response.access_token;
704 | ```
705 |
706 | 5. Now we implement the regenerateSessionAsync method. This method simply wraps the generation of the session in a Promise. We do this because we don’t want to mix awaits and callbacks. If we had to do this more often we would use a wrapping library, but this is the only time, so we do it in the rough way. 💡 you can read more about asynchronous calls [here](https://strongloop.com/strongblog/async-error-handling-expressjs-es7-promises-generators/)
707 |
708 | **controller.js** regenerateSessionAsync **method**
709 |
710 | ```javascript
711 | //Returns a promise that fulfills when a new session is created
712 | function regenerateSessionAsync(req){
713 | return new Promise((resolve,reject)=>{
714 | req.session.regenerate((err)=>{
715 | err ? reject(err) : resolve();
716 | });
717 | });
718 | }
719 | ```
720 |
721 | And you can now run with `npm start`.
722 | 🎯 The source code at this point can be found in [this link](https://github.com/dropbox/dbximages/tree/backendwithsession)
723 |
724 |
725 | # 7. Deploying to Heroku
726 |
727 | **⚠️ While you will not be charged anything for following any of the steps below, provisioning the Redis database addin requires you to have a credit card on file on Heroku. But again,** **we will be using only free tiers.**
728 |
729 | 
730 |
731 |
732 |
733 | - First create a free account at https://heroku.com
734 | - If they ask you about the technology you will be using, select Node.JS
735 | - Then click on **Create an App** and give it a name. In this specific case, I will call it **dbximgs** as I was lucky enough to have that name still available.
736 |
737 |
738 | Once you create the app, you are pretty much given the instructions to put the code there, **but don’t do all the steps** just yet as you need to make several changes.
739 |
740 |
741 | 1. Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line) and login
742 |
743 |
744 | heroku login
745 |
746 |
747 | 2. A the root level of your project, initialize a git repository. Make sure you use your own project name.
748 | git init
749 | heroku git:remote -a dbximgs
750 |
751 |
752 | 3. Now we need to add a .gitignore file at the root level so we don’t upload to Heroku all the libraries or files we want to leave behind for security purposes.
753 |
754 | **.gitignore**
755 |
756 | # Node build artifacts
757 | node_modules
758 | npm-debug.log
759 |
760 | # Local development
761 | *.env
762 | package-lock.json
763 |
764 |
765 | 4. There is information that should not be hardcoded in the source code like the Dropbox client/secret and also the Redis secret. Also, there are items that should be configured for a local test vs server production such as the redirectURL of the OAuth flow. There are several ways to work on this, like the *Heroku local* command, but to have independence of Heroku for local testing, we will use the **dotenv** library which is not much different.
766 |
767 | This library pushes a set of environment variables when the server starts from a **.env** file if found. We will have a .env file only in the local environment but we will not push it to Heroku as stated in the .gitignore above. Heroku instead uses configuration variables to feed the same information.
768 |
769 | First, let us install the dotenv library
770 |
771 |
772 | npm install dotenv --save
773 |
774 | Then, let us add a .env file to the root of the project
775 | **.env**
776 |
777 | DBX_APP_KEY=''
778 | DBX_APP_SECRET=''
779 | OAUTH_REDIRECT_URL='http://localhost:3000/oauthredirect'
780 | SESSION_ID_SECRET='cAt2-D0g-cAW'
781 |
782 | Finally, replace the whole config file to the code below. Notice how we are reading several variables from the environment (which is loaded at startup from the .env file)
783 | **config.js**
784 |
785 | ```javascript
786 | require('dotenv').config({silent: true});
787 |
788 | module.exports = {
789 | DBX_API_DOMAIN: 'https://api.dropboxapi.com',
790 | DBX_OAUTH_DOMAIN: 'https://www.dropbox.com',
791 | DBX_OAUTH_PATH: '/oauth2/authorize',
792 | DBX_TOKEN_PATH: '/oauth2/token',
793 | DBX_LIST_FOLDER_PATH:'/2/files/list_folder',
794 | DBX_LIST_FOLDER_CONTINUE_PATH:'/2/files/list_folder/continue',
795 | DBX_GET_TEMPORARY_LINK_PATH:'/2/files/get_temporary_link',
796 | DBX_APP_KEY:process.env.DBX_APP_KEY,
797 | DBX_APP_SECRET:process.env.DBX_APP_SECRET,
798 | OAUTH_REDIRECT_URL:process.env.OAUTH_REDIRECT_URL,
799 | SESSION_ID_SECRET:process.env.SESSION_ID_SECRET,
800 | }
801 | ```
802 |
803 | Since those variables won’t exist on Heroku, we need to manually add them. So in your app in Heroku, click on **Settings** and then on **Reveal config vars**
804 |
805 | 
806 |
807 |
808 | Then manually add the variables with the proper values
809 |
810 | 
811 |
812 |
813 | 👁️ Notice something important here. We changed the OAUTH_REDIRECT_URL to https://dbximgs.herokuapp.com/oauthredirect. In this field you need to put the name of your app in the following way:
814 |
815 | https://.herokuapp.com/oauthredirect
816 |
817 |
818 | 5. As we are now using a different redirect URL for the authentication, we need to also add it to the [Dropbox app console.](https://www.dropbox.com/developers/apps/)
819 |
820 |
821 | 
822 |
823 |
824 |
825 | 6. When you deploy your app to Heroku, it installs all the libraries and dependencies from your package.json file, but the problem we will see is that Node.JS itself might be a version not compatible yet with elements of ES7 we put in our code like the async/await calls. To avoid this, we need to set the Node.JS dependency in the package.json file. For this add the following lines right before the **dependencies**
826 |
827 | **package.json**
828 |
829 | "engines": {
830 | "node": "~8.2.1"
831 | },
832 |
833 | It will look something like this
834 | **package.json**
835 |
836 | {
837 | "name": "dbximgs",
838 | "version": "0.0.0",
839 | "private": true,
840 | "scripts": {
841 | "start": "node ./bin/www"
842 | },
843 | "engines": {
844 | "node": "~8.2.1"
845 | },
846 | "dependencies": {
847 | //many libraries listed here
848 | }
849 | }
850 |
851 |
852 |
853 | 7. We use Redis to store sessions and luckily there is a free Heroku addin that can be configured only with a few steps:
854 |
855 |
856 | 
857 |
858 |
859 |
860 | - First, go to the [Heroku Redis addin page](https://elements.heroku.com/addons/heroku-redis)
861 | - Click on **Install Heroku Redis** button
862 | - Select your application from the dropdown list
863 | - Choose the free tier and press **provision**. If you don’t have a credit card on file this will fail, so you will need to add one to continue in the billing settings of your profile.
864 |
865 |
866 |
867 | Now you will see the addin in your app
868 |
869 | 
870 |
871 |
872 | There is one more step you need to change in your code for Redis to work. You need to add an environment variable when you create the database client in the app.js file. When this runs locally, this value will be empty, but when Heroku calls it, it will add a variable it has added to our config vars when you deployed the plugin.
873 |
874 | **app.js**
875 |
876 | ```javascript
877 | //var client = redis.createClient();
878 | var client = redis.createClient(process.env.REDIS_URL);
879 | ```
880 |
881 | You can see it yourself in the settings page of the Heroku app if you click on Reveal config vars
882 |
883 |
884 | 8. Seems we have all the elements in place to push the magic button. (make sure you are logged to Heroku in your console, otherwise run the `heroku login` command at the root level of your project.
885 |
886 | now run
887 |
888 | git add --all
889 | git commit -m "Heroku ready code"
890 | git push heroku master
891 |
892 |
893 | which will start the deploy and will show you something like this
894 |
895 |
896 | 
897 |
898 |
899 | You can also check the Heroku logs to make sure server is running correctly
900 |
901 | heroku logs --tail
902 |
903 | Something like this means things are working fine
904 |
905 | 
906 |
907 |
908 |
909 | 9. You can now go and test your app!! 🤡
910 |
911 | If you are not sure of the link, you can start it from the Heroku console using the Open App button. Or run the `heroku open` command in the console.
912 |
913 |
914 | 
915 |
916 |
917 | It will be something like this
918 |
919 | https://.herokuapp.com
920 |
921 |
922 | 👁️ Remember to add some images to the folder if the Dropbox account you are linking is new. Otherwise, you will simply see a sad white page.
923 | 🎯 The source code at this point can be found in [this link](https://github.com/dropbox/dbximages/tree/herokuready)
924 |
925 |
926 | # 8. Security considerations and actions
927 |
928 | In general, there are a set of security measures we can take to protect our app. I am checking the ones we have already implemented.
929 |
930 | **Oauth**
931 |
932 | - 💚 OAuth code flow where token is not exposed to the Web browser
933 | - 💚 Check state parameter on the OAuth flow to avoid CSRF
934 | - 💚 Store token on a fresh session (regenerate the session)
935 |
936 | **Cookies**
937 |
938 | - 💚 Cookies: disabling scripts to read/write cookies from browser in the session configuration. By default the HttpOnly attribute is set.
939 | - 💚 Cookies: enforcing same domain origin (default on the session configuration)
940 | - 🔴 Cookies: make sure cookies are transported only via https. 😱 we will fix it later.
941 |
942 | **Securing headers**
943 |
944 | - Good information in [this blog post](https://blog.risingstack.com/node-js-security-checklist/), but here is the summary of what we should care about.
945 | - 🔴 **Strict-Transport-Security** enforces secure (HTTP over SSL/TLS) connections to the server
946 | - 🔴 **X-Frame-Options** protection against clickjacking or disallowing to be iframed on another site.
947 | - 🔴 **X-XSS-Protection** Cross-site scripting (XSS) filter
948 | - 🔴 **X-Content-Type-Options** prevents browsers from MIME-sniffing a response away from the declared content-type
949 | - 🔴 **Content-Security-Policy** prevents a wide range of attacks, including Cross-site scripting and other cross-site injections
950 |
951 | The blogpost above has more security considerations if you intend to go deeper on the topic.
952 |
953 | To secure the headers above, we will use the [helmet](https://www.npmjs.com/package/helmet) library.
954 | 💡 This is a [good blog post to read about helmet](http://scottksmith.com/blog/2014/09/21/protect-your-node-apps-noggin-with-helmet/)
955 |
956 |
957 | npm install helmet --save
958 |
959 | And add the following code to the app.js file to set the headers. Notice that we are only allowing scripts from the ajax.googleapi (this is where we find the jQuery library). Another option is to simply copy the file locally, for that, change the reference in the page.js file.
960 |
961 | **app.js**
962 |
963 | ```javascript
964 | var helmet = require('helmet');
965 |
966 | //Headers security!!
967 | app.use(helmet());
968 |
969 | // Implement CSP with Helmet
970 |
971 | app.use(helmet.contentSecurityPolicy({
972 | directives: {
973 | defaultSrc: ["'self'"],
974 | scriptSrc: ["'self'","https://ajax.googleapis.com/"],
975 | styleSrc: ["'self'"],
976 | imgSrc: ["'self'","https://dl.dropboxusercontent.com"],
977 | mediaSrc: ["'none'"],
978 | frameSrc: ["'none'"]
979 | },
980 |
981 | // Set to true if you want to blindly set all headers: Content-Security-Policy,
982 | // X-WebKit-CSP, and X-Content-Security-Policy.
983 | setAllHeaders: true
984 |
985 | }));
986 | ```
987 |
988 | With this we have secured the headers now 🤠
989 |
990 | - 💚 Securing headers
991 |
992 | Let us know fix the cookie transport issue. The best thing to do here is to enable http for development purposes and only allow https for production. Development and production can be set with the NODE_ENV env variable. Heroku is by defauld set to production, the local host is development by default. You can modify this behavior [following these steps](https://devcenter.heroku.com/articles/nodejs-support#devdependencies)
993 |
994 | After the sess variable is initialized (before the app.use) in the apps.js add the following code
995 | **app.js**
996 |
997 | ```javascript
998 | //cookie security for production: only via https
999 | if (app.get('env') === 'production') {
1000 | app.set('trust proxy', 1) // trust first proxy
1001 | sess.cookie.secure = true // serve secure cookies
1002 | }
1003 | ```
1004 |
1005 | It is important to do the *trust the first proxy* for Heroku as any requests enters via https to Heroku but the direct internal call to our middleware is http via some load balancer.
1006 |
1007 | And we are done! 👊
1008 |
1009 | - 💚 Cookies: make sure cookies are transported only via https
1010 |
1011 | Now you want to push this to heroku
1012 |
1013 | git add --all
1014 | git commit -m "security improvements"
1015 | git push heroku master
1016 |
1017 |
1018 | 🎯 The source code at this point can be found in [this link](https://github.com/dropbox/dbximages/tree/herokusecure)
1019 |
1020 |
1021 | ## Checking dependency vulnerabilities
1022 |
1023 | The great thing about Node.JS is that you usually find a library that does exactly what you want. But it comes to a great cost, libraries get outdated and then you find yourself in 🔥 *library hell* 🔥 when they have specific vulnerabilities that get patched. Think about it, you have dependencies that have dependencies and suddenly you have hundreds of dependencies and libraries that might be outdated and vulnerable.
1024 |
1025 | A good way to check you are protected against vulnerabilities to specific dependencies in your code is the `nsp check` command. It will tell you which libraries have been compromised, what are the patched versions, where to find them and the version you have.
1026 |
1027 | so run
1028 |
1029 |
1030 | nsp check
1031 |
1032 | and you will get a bunch of tables that look like this
1033 |
1034 |
1035 | 
1036 |
1037 |
1038 | As with Heroku we don’t actually push the **node_modules** package, it makes sense to patch those files that are directly stated in the **package.json** file by simply changing the version required. But make sure you test in case there were major changes for that dependency. Every time you push your code to Heroku, it runs the `npm install` command recreating the node_modules folder.
1039 |
1040 | If the vulnerability is in a library within one of your projects dependencies, check if updating the dependency will fix the issue. Otherwise you have three options: accept the risk of keeping it, replace the library for a similar without the vulnerability or finally, patch it yourself and then actually push all the libraries to Heroku yourself.
1041 |
1042 |
1043 | # 9. Production and things to do from here
1044 |
1045 | The master branch of this tutorial also includes a method to logout which revokes the token and deletes the session, it also removes some unused files. [check it out.](https://github.com/dropbox/dbximages/tree/master)
1046 |
1047 | This is an additional list of things to do to make this tutorial fully production material.
1048 |
1049 |
1050 | 1. Manage the errors: for this tutorial, all the errors will be propagated to the app.js page that will set the right error code on the http response and display the error template.
1051 | res.render('error');
1052 |
1053 | You could make that error page look nicer.
1054 |
1055 |
1056 | 3. The front end was barely worked, you can make it look much nicer.
1057 | 4. Add app icons to the Dropbox page so the authorization looks better.
1058 | 5. Probably the most important one will be pagination on the images. You can call Dropbox specifying a number of items you want back. Then Dropbox will give you a cursor to iterate. Adding that will just make this tutorial much longer, so that’s a good task for you if you want to extend it.
1059 |
1060 |
1061 | # 10. Try it yourself
1062 |
1063 | [https://dbximgs.herokuapp.com](https://dbximgs.herokuapp.com)
1064 |
1065 |
1066 | 🤓 After you hit the link, give it a couple minutes since free Heroku instances are dormant by default and need to be restarted.
1067 |
1068 | You can revoke the token and delete the session using
1069 | [https://dbximgs.herokuapp.com/logout](https://dbximgs.herokuapp.com/logout)
1070 |
1071 |
1072 | # License
1073 |
1074 | Apache 2.0
1075 |
1076 |
1077 |
1078 |
--------------------------------------------------------------------------------