├── .gitignore ├── .travis.yml ├── README.md ├── cert.pem ├── client.js ├── cookies.json ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── spec.js └── support │ ├── commands.js │ ├── defaults.js │ └── index.js ├── key.pem ├── next-update-travis.sh ├── package.json ├── server.js └── views ├── form.pug └── index.pug /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | sessions/ 3 | cookies 4 | npm-debug.log 5 | cypress/screenshots/ 6 | cypress/videos/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: true 8 | node_js: 9 | - '6' 10 | before_script: 11 | - npm prune 12 | - npm install -g cypress-cli 13 | script: 14 | # upgrade dependencies if a CRON job 15 | - ./next-update-travis.sh 16 | # tests server running locally 17 | - npm start & 18 | - cypress ci 19 | # deploy and test "production" server 20 | - npm install -g now-pipeline@1.8.0 21 | - now-pipeline-list 22 | - echo Trying to deploy new stuff 23 | - now-pipeline --test "npm run prod-test" 24 | # - list 25 | # - prune 26 | branches: 27 | except: 28 | - "/^v\\d+\\.\\d+\\.\\d+$/" 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-sessions-tutorial 2 | 3 | > Debugging ExpressJS sessions tutorial 4 | 5 | [![Build status][ci-image]][ci-url] 6 | [![next-update-travis badge][nut-badge]][nut-readme] 7 | 8 | You can run the code at different step, here are the tags 9 | 10 | * `step-0` - the initial code without sessions. 11 | * `step-1` - sessions in plain JSON files. 12 | * `step-2` - counting views in a session. 13 | * `step-3` - sending cookies from curl or [httpie](https://github.com/jkbrzt/httpie) 14 | * `step-4` - session from a Node client. 15 | * `step-5` - Express server running on HTTPS locally. 16 | * `step-6` - updated code for connecting to self-signed HTTPS servers. 17 | * `step-7` - passing Referer header from server's response to the next request 18 | * `step-8` - protecting a form using CSRF attached to the session object 19 | * `step-9` - protecting JSON POST request with CSRF header 20 | * `step-10` - CSRF using cookie, not session 21 | * `step-11` - pass HOST and cookie options to set explicit domain 22 | * `step-12` - session and CSRF cookies are set with flags 23 | [HttpOnly and SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). 24 | 25 | The tutorial itself is at [glebbahmutov.com/blog/express-sessions/](http://glebbahmutov.com/blog/express-sessions/). 26 | 27 | ## To use 28 | 29 | ```sh 30 | git clone git@github.com:bahmutov/express-sessions-tutorial.git 31 | cd express-sessions-tutorial 32 | git checkout step-0 33 | npm install 34 | npm start 35 | ``` 36 | 37 | The open the displayed url, usually `localhost:3000` in the browser or make 38 | requests using `curl` or `http` following the blog post. 39 | 40 | You can switch to another step, but probably need to run `npm install` 41 | command. 42 | 43 | ```sh 44 | git checkout step-10 45 | npm install 46 | npm start 47 | ``` 48 | 49 | ## Running with subdomains 50 | 51 | In order to test with more than just `localhost:3000` domain, you could 52 | setup subdomains using [express-subdomain](https://www.npmjs.com/package/express-subdomain). 53 | 54 | In order to use the subdomain we need to map domain names to local HTTP 55 | address. For example using `/etc/hosts` 56 | 57 | ``` 58 | 127.0.0.1 gleb.dev 59 | 127.0.0.1 forms.gleb.dev 60 | ``` 61 | 62 | There is a weird issue testing this project using Cypress against domains, 63 | see [issues/4](https://github.com/bahmutov/express-sessions-tutorial/issues/4). 64 | 65 | ### Small print 66 | 67 | Author: Gleb Bahmutov © 2015 68 | 69 | * [@bahmutov](https://twitter.com/bahmutov) 70 | * [glebbahmutov.com](http://glebbahmutov.com) 71 | * [blog](http://glebbahmutov.com/blog/) 72 | 73 | License: MIT - do anything with the code, but don't blame me if it does not work. 74 | 75 | Spread the word: tweet, star on github, etc. 76 | 77 | Support: if you find any problems with this module, email / tweet / open issue on Github 78 | 79 | ## MIT License 80 | 81 | Copyright (c) 2015 Gleb Bahmutov 82 | 83 | Permission is hereby granted, free of charge, to any person 84 | obtaining a copy of this software and associated documentation 85 | files (the "Software"), to deal in the Software without 86 | restriction, including without limitation the rights to use, 87 | copy, modify, merge, publish, distribute, sublicense, and/or sell 88 | copies of the Software, and to permit persons to whom the 89 | Software is furnished to do so, subject to the following 90 | conditions: 91 | 92 | The above copyright notice and this permission notice shall be 93 | included in all copies or substantial portions of the Software. 94 | 95 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 96 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 97 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 98 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 99 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 100 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 101 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 102 | OTHER DEALINGS IN THE SOFTWARE. 103 | 104 | [ci-image]: https://travis-ci.org/bahmutov/express-sessions-tutorial.svg?branch=master 105 | [ci-url]: https://travis-ci.org/bahmutov/express-sessions-tutorial 106 | [nut-badge]: https://img.shields.io/badge/next--update--travis-ok-green.svg 107 | [nut-readme]: https://github.com/bahmutov/next-update-travis#readme 108 | -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXTCCAkWgAwIBAgIJAMYT1CPorFEaMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTUwODIwMjE1MDQ4WhcNMTYwODE5MjE1MDQ4WjBF 5 | MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEA7TEApyQuzVKXX3mmLM1ppoV4g6yYPjsuJxQ4bb9CGqWLY9rTO+p0IVPN 8 | QmHIfj6LXmE96rbN7DJ6pO0xr20sDBFcpy7mCAKk3Awn0d+8D3LN6cU+d0OYZNDW 9 | faudXjLO/PO3wIEkoXOQmJdZH48g4zbFAE8ZKmD7RVPgTRCz3Cb6t3LtS0bzhyil 10 | hk0HeDka2XDbAQOXrLp/Sg05uaBfYqf2ZxKrEE6+NkAxTXniZWv+v2FMOp6NVPW9 11 | 3sw3Y6bCATLkUVgPAfhzM/3OYgyq5ZmfN28YQL1un9MO6zagL1ZV/yAID1fano9s 12 | Pfd7vkLGH0zjv2D0r8CZC3YMWnxP+wIDAQABo1AwTjAdBgNVHQ4EFgQUoca0tsO5 13 | aghlBeRM7LeRp77+N5UwHwYDVR0jBBgwFoAUoca0tsO5aghlBeRM7LeRp77+N5Uw 14 | DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAsuwWnLa8niW3Cn13XZM2 15 | wl/dQ4CikqwbS67ZiChEjgmaOdEQFTOeesFMpoQHcPMDS6rls5qa/Ro4424Tuf/q 16 | byLl/D8q9lw+h2orVQ3uAbKHk92yVMaNg4gTzWvPjIf4Uq9vlbddf84pZvPLekc7 17 | nl0vp3aUKYSRXp8TR6RcRm1A7z+wH53iiGid4q+elj1hDEM7lkRXe2S+a2D+7h27 18 | y7CFnKfHjdibKfldn36Zxt9Uq4/Hr9kRaq0dX3hbU/rVLw135Z5To1v+REG4EJRj 19 | BRvXiI42TeMxWOcWbFBWIGuw2oZZ9PppZ3jvd6NwaN/DyGnmB/E+iDWqK4DaNLj+ 20 | tw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var FileCookieStore = require('tough-cookie-filestore'); 2 | var requestPromise = require('request-promise'); 3 | var rp = requestPromise.defaults({ 4 | strictSSL: false, // allow us to use our self-signed cert for testing 5 | rejectUnauthorized: false, 6 | jar: requestPromise.jar(new FileCookieStore('cookies.json')) 7 | }); 8 | 9 | function requestPage(previousResponse) { 10 | var referer = previousResponse ? previousResponse.headers.referer : null; 11 | if (previousResponse) { 12 | console.log('previous response referer "%s"', referer); 13 | } 14 | 15 | return rp({ 16 | url: 'https://localhost:3000/', 17 | resolveWithFullResponse: true, 18 | headers: { 19 | referer: referer 20 | } 21 | }); 22 | } 23 | 24 | requestPage() 25 | .then(function (response) { 26 | console.log(response.body); 27 | return requestPage(response); 28 | }) 29 | .then(function (response) { 30 | console.log(response.body); 31 | return requestPage(response); 32 | }) 33 | .catch(console.error); 34 | -------------------------------------------------------------------------------- /cookies.json: -------------------------------------------------------------------------------- 1 | {"localhost":{"/":{"server-session-cookie-id":{"key":"server-session-cookie-id","value":"s%3AhpkBI3SWnbKZQaKE7Gj2BDYFYJ1PZ0PV.HofW9TqYB%2BRoMp%2FXWWBx5lIPbYnEYfMdciVTz0%2F6XOU","domain":"localhost","path":"/","httpOnly":true,"hostOnly":true,"creation":"2015-08-20T21:42:46.878Z","lastAccessed":"2015-08-21T15:25:23.354Z"}}}} -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "50d090a2-03b8-49fc-a520-0ed3da5b8c30" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | describe('Express Session', function(){ 2 | 3 | const baseUrl = Cypress.env('HOST') || 'http://localhost:3000' 4 | 5 | beforeEach(function () { 6 | cy.visit(baseUrl) 7 | }) 8 | 9 | it('starts without sessions', function(){ 10 | cy.title().should('include', 'First visit') 11 | }) 12 | 13 | it('increments the session counter on each visit', function(){ 14 | cy.visit(baseUrl) 15 | cy.title().should('include', 'Index page') 16 | cy.contains('.views', '1') 17 | cy.visit(baseUrl) 18 | cy.contains('.views', '2') 19 | }) 20 | 21 | it('can submit a form', function () { 22 | cy.contains('a', 'Visit') 23 | .click() 24 | cy.url().should('contain', '/form') 25 | cy.get('input[name="name"]') 26 | .type('foo') 27 | cy.get('button[type="submit"]') 28 | .click() 29 | 30 | cy.url().should('equal', baseUrl + '/') 31 | cy.title().should('include', 'Index page') 32 | cy.contains('.views', '1') 33 | }) 34 | 35 | it('can execute POST fetch', () => { 36 | cy.contains('a', 'Visit') 37 | .click() 38 | cy.get('button#fetch') 39 | .click() 40 | cy.contains('#status', 'ok') 41 | }) 42 | 43 | }) 44 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create the custom command: 'login'. 4 | // 5 | // The commands.js file is a great place to 6 | // modify existing commands and create custom 7 | // commands for use throughout your tests. 8 | // 9 | // You can read more about custom commands here: 10 | // https://on.cypress.io/api/commands 11 | // *********************************************** 12 | // 13 | // Cypress.addParentCommand("login", function(email, password){ 14 | // var email = email || "joe@example.com" 15 | // var password = password || "foobar" 16 | // 17 | // var log = Cypress.Log.command({ 18 | // name: "login", 19 | // message: [email, password], 20 | // consoleProps: function(){ 21 | // return { 22 | // email: email, 23 | // password: password 24 | // } 25 | // } 26 | // }) 27 | // 28 | // cy 29 | // .visit("/login", {log: false}) 30 | // .contains("Log In", {log: false}) 31 | // .get("#email", {log: false}).type(email, {log: false}) 32 | // .get("#password", {log: false}).type(password, {log: false}) 33 | // .get("button", {log: false}).click({log: false}) //this should submit the form 34 | // .get("h1", {log: false}).contains("Dashboard", {log: false}) //we should be on the dashboard now 35 | // .url({log: false}).should("match", /dashboard/, {log: false}) 36 | // .then(function(){ 37 | // log.snapshot().end() 38 | // }) 39 | // }) -------------------------------------------------------------------------------- /cypress/support/defaults.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example defaults.js shows you how to 3 | // customize the internal behavior of Cypress. 4 | // 5 | // The defaults.js file is a great place to 6 | // override defaults used throughout all tests. 7 | // 8 | // *********************************************** 9 | // 10 | // Cypress.Server.defaults({ 11 | // delay: 500, 12 | // whitelist: function(xhr){} 13 | // }) 14 | 15 | // Cypress.Cookies.defaults({ 16 | // whitelist: ["session_id", "remember_token"] 17 | // }) -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your other test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/guides/configuration#section-global 14 | // *********************************************************** 15 | 16 | // Import commands.js and defaults.js 17 | // using ES2015 syntax: 18 | import "./commands" 19 | import "./defaults" 20 | 21 | // Alternatively you can use CommonJS syntax: 22 | // require("./commands") 23 | // require("./defaults") 24 | -------------------------------------------------------------------------------- /key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA7TEApyQuzVKXX3mmLM1ppoV4g6yYPjsuJxQ4bb9CGqWLY9rT 3 | O+p0IVPNQmHIfj6LXmE96rbN7DJ6pO0xr20sDBFcpy7mCAKk3Awn0d+8D3LN6cU+ 4 | d0OYZNDWfaudXjLO/PO3wIEkoXOQmJdZH48g4zbFAE8ZKmD7RVPgTRCz3Cb6t3Lt 5 | S0bzhyilhk0HeDka2XDbAQOXrLp/Sg05uaBfYqf2ZxKrEE6+NkAxTXniZWv+v2FM 6 | Op6NVPW93sw3Y6bCATLkUVgPAfhzM/3OYgyq5ZmfN28YQL1un9MO6zagL1ZV/yAI 7 | D1fano9sPfd7vkLGH0zjv2D0r8CZC3YMWnxP+wIDAQABAoIBAQCKjNc+yunz0czO 8 | XnbtMMgIF2sAL2922obpGOylXtU0T4MOvyIom5leZl896XR+Gfa7GL5cPpAm6o2t 9 | jUg6muDh47plhgWkpDa8uvT/qVtnAr65URhd/kQkj8DbA8YW4kL6izrI3icRkDnk 10 | iHPs9WRWlQWaWpnuoVvlcUtSePE3JQXBlS1QWw39rVYpZeqNWprhBKSjj6i1OHZI 11 | 9b4ZTD+Ry9SlMNmVT57RHJB5SoBHmJAy41QEuswJeJnQT8o4+YyxYEttYhXPEoug 12 | F2zCLJpHfEfvHZrwSyeeySB7+iTtRI+Nb1AAw7x7asbDn1US7XzIB/YtW55ok1AY 13 | UZ8orKWBAoGBAP6gbPM7ZAgJT7ekuQE3muEMGFx/kUg7OTSTO8yZ73kjlIb1R1/f 14 | x10xv+CeIIkp44fAMlNOcy+y9gUdRFrcqmRak4EpiFYYFqQ2uVrpQ3PKSh2ytdl1 15 | CVzQKFp+nfBF7O3Fm3Kuj3Xcs8mmWjtsY4Fp/oX24kyHItzK2rsw72bbAoGBAO54 16 | gNrnGvTZSvldaVw91BVEBSx6mMlpjYT2sbVaJfkvLX/H/uaE5mq1iu/vbspoBi+U 17 | vVLg6oIMsiQzN028mv0igNyLRFbieGL5IK6hLlmEvMrT50DDLuoQErWRKG3z97Si 18 | Aa2m8mGGvAsjM9snZCUzh28DlZh4Gi4SPUxb4TVhAoGBANQGajKwBb/bYRIejB9D 19 | WiiDldWQND3dcukgoO7iT9Kjmg43OFPRV4V247v6cEVHKDvmAwHzlV7muo3PrRes 20 | IAaolaM8HlbygAgFuZrGGnDUxZqtMVf+aOlsO+3++S0WTRBBOAvq53LRcLQ9XW2V 21 | 99XPmS2cQxxOeu03zaOKQA95AoGBAJVm87uxXIcX98vhBRhgOBYWpnMmX8CYG6y4 22 | 7b8junSyZPwQbZc4ni8ui9wkkrHGmFGJC0/4T5Ooppbda4GNb0C2NCt4KRmSC0Be 23 | umYN3z8AVVNxjQla/3JvHXmZds3kMkV91jVYSbRmODt2E4/yzuArt2cKxNdgL267 24 | yKGlUqQhAoGAPr1eKnEFiLWV0HE0ySBPjWv4KP2Mu82OGjL3WfslrrLs4fB6VEXS 25 | Ek/34XJ/WaeAZYwpVUpQqT2+xJ3a/CnzLoFQPOc/8iaQ+f4kXhw4pmklFR06KGRg 26 | Q/TR8Meg966VBcWOUqMoepDxorHuoZz3ap2tlkgfsELkVuSoApfop+4= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /next-update-travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then 6 | if [ "$GH_TOKEN" = "" ]; then 7 | echo "" 8 | echo "⛔️ Cannot find environment variable GH_TOKEN ⛔️" 9 | echo "Please set it up for this script to be able" 10 | echo "to push results to GitHub" 11 | echo "ℹ️ The best way is to use semantic-release to set it up" 12 | echo "" 13 | echo " https://github.com/semantic-release/semantic-release" 14 | echo "" 15 | echo "npm i -g semantic-release-cli" 16 | echo "semantic-release-cli setup" 17 | echo "" 18 | exit 1 19 | fi 20 | 21 | echo "Upgrading dependencies using next-update" 22 | npm i -g next-update 23 | 24 | # you can edit options to allow only some updates 25 | # --allow major | minor | patch 26 | # --latest true | false 27 | # see all options by installing next-update 28 | # and running next-update -h 29 | next-update --allow minor --latest false 30 | 31 | git status 32 | # if package.json is modified we have 33 | # new upgrades 34 | if git diff --name-only | grep package.json > /dev/null; then 35 | echo "There are new versions of dependencies 💪" 36 | git add package.json 37 | echo "----------- package.json diff -------------" 38 | git diff --staged 39 | echo "-------------------------------------------" 40 | git config --global user.email "next-update@ci.com" 41 | git config --global user.name "next-update" 42 | git commit -m "chore(deps): upgrade dependencies using next-update" 43 | # push back to GitHub using token 44 | git remote remove origin 45 | # TODO read origin from package.json 46 | # or use github api module github 47 | # like in https://github.com/semantic-release/semantic-release/blob/caribou/src/post.js 48 | git remote add origin https://next-update:$GH_TOKEN@github.com/bahmutov/express-sessions-tutorial.git 49 | git push origin HEAD:master 50 | else 51 | echo "No new versions found ✋" 52 | fi 53 | else 54 | echo "Not a cron job, normal test" 55 | fi 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-sessions-tutorial", 3 | "version": "0.0.0-development", 4 | "description": "ExpressJS session tutorial", 5 | "main": "index.js", 6 | "files": [ 7 | "server.js", 8 | "views" 9 | ], 10 | "scripts": { 11 | "test": "run-p --race start e2e", 12 | "start": "node server.js", 13 | "e2e": "cypress run", 14 | "watch": "nodemon server.js --ignore sessions", 15 | "make-certificate": "openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365", 16 | "remove-passphrase": "openssl rsa -in key.pem -out newkey.pem && mv newkey.pem key.pem", 17 | "prod-test": "CYPRESS_HOST=$NOW_URL cypress ci", 18 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 19 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 20 | }, 21 | "keywords": [ 22 | "express", 23 | "tutorial" 24 | ], 25 | "author": "Gleb Bahmutov ", 26 | "license": "MIT", 27 | "dependencies": { 28 | "body-parser": "1.17.2", 29 | "cookie-parser": "1.4.3", 30 | "csurf": "1.9.0", 31 | "express": "4.15.4", 32 | "express-session": "1.15.5", 33 | "express-subdomain": "1.0.5", 34 | "morgan": "1.8.2", 35 | "pug": "2.0.0-beta6", 36 | "request-promise": "4.2.1", 37 | "session-file-store": "1.1.2", 38 | "tough-cookie-filestore": "0.0.1" 39 | }, 40 | "devDependencies": { 41 | "next-update-travis": "1.7.1", 42 | "nodemon": "1.11.0", 43 | "npm-run-all": "4.1.1", 44 | "semantic-release": "^6.3.6" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/bahmutov/express-sessions-tutorial.git" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var host = process.env.HOST || 'localhost' 4 | var port = process.env.PORT || 3000 5 | var useDomainForCookies = process.env.DOMAIN || false 6 | 7 | var http = require('http'); 8 | var express = require('express'); 9 | var app = express(); 10 | 11 | app.use(require('morgan')('dev')); 12 | 13 | app.set('views', './views') 14 | app.set('view engine', 'pug') 15 | 16 | var session = require('express-session'); 17 | var FileStore = require('session-file-store')(session); 18 | var bodyParser = require('body-parser'); 19 | var csrf = require('csurf'); 20 | var cookieParser = require('cookie-parser'); 21 | 22 | var subdomain = require('express-subdomain'); 23 | var router = express.Router(); 24 | 25 | app.use(session({ 26 | name: 'server-session-cookie-id', 27 | secret: 'my express secret', 28 | saveUninitialized: true, 29 | resave: true, 30 | store: new FileStore({ 31 | path: '/tmp' 32 | }), 33 | cookie: { 34 | sameSite: true, 35 | domain: useDomainForCookies ? host : undefined 36 | } 37 | })); 38 | 39 | var csrfProtection = csrf({ 40 | cookie: { 41 | key: '_csrf', 42 | sameSite: true, 43 | httpOnly: true, 44 | domain: useDomainForCookies ? host : undefined 45 | } 46 | }) 47 | 48 | // we need this because "cookie" is true in csrfProtection 49 | app.use(cookieParser()) 50 | 51 | // parse application/x-www-form-urlencoded 52 | app.use(bodyParser.urlencoded({ extended: false })) 53 | // parse JSON bodies 54 | app.use(bodyParser.json()) 55 | 56 | app.get('/', function initViewsCount(req, res, next) { 57 | if (typeof req.session.views === 'undefined') { 58 | req.session.views = 0; 59 | return res.render('index', { 60 | pageTitle: 'First visit', 61 | host: host, 62 | port: port, 63 | views: req.session.views 64 | }); 65 | } 66 | return next(); 67 | }); 68 | 69 | app.get('/', function incrementViewsCount(req, res, next) { 70 | console.assert(typeof req.session.views === 'number', 71 | 'missing views count in the session', req.session); 72 | req.session.views++; 73 | return next(); 74 | }); 75 | 76 | app.use(function printSession(req, res, next) { 77 | console.log('req.session', req.session); 78 | console.log('req header referer', req.header('Referer')); 79 | console.log('req header x-token', req.header('x-token')); 80 | return next(); 81 | }); 82 | 83 | app.get('/', function sendPageWithCounter(req, res) { 84 | res.render('index', { 85 | pageTitle: 'Index page', 86 | host: host, 87 | port: port, 88 | views: req.session.views 89 | }); 90 | }); 91 | 92 | // 93 | // form will be in the sub domain 94 | // 95 | 96 | // protect form with CSRF 97 | router.get('/form', csrfProtection, function (req, res) { 98 | // csrfProtection can create a new token for us to insert 99 | // into the FROM as a hidden input field 100 | res.render('form', { 101 | pageTitle: 'Form', 102 | csrfToken: req.csrfToken() 103 | }) 104 | }); 105 | // receive form submission 106 | router.post('/process', csrfProtection, function (req, res) { 107 | console.log('form submission', req.body) 108 | res.redirect('/') 109 | }) 110 | 111 | // receive JSON post 112 | router.post('/fetch', csrfProtection, function (req, res) { 113 | console.log('fetch POST', req.body) 114 | res.send({status: 'ok'}) 115 | }) 116 | // app.use(subdomain('forms', router)) 117 | app.use(router); 118 | 119 | var server = http.createServer(app).listen(port, function () { 120 | console.log('Example app listening at http://%s:%s', host, port); 121 | }); 122 | -------------------------------------------------------------------------------- /views/form.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= pageTitle 5 | body 6 | h1 A form 7 | #container.col 8 | p. 9 | This form will insert CSRF token. Note that 10 | it is a different token every time you reload the page 11 | pre #{csrfToken} 12 | 13 | form(action="/process", method="POST") 14 | input(type="hidden", name="_csrf", value=`${csrfToken}`) 15 | 16 | p You name: 17 | input(type="text", name="name") 18 | button(type="submit") Submit 19 | 20 | h2 JSON fetch request with CSRF 21 | button#fetch Fetch 22 | div#status Status will be here 23 | 24 | script. 25 | function setStatus(s) { 26 | document.getElementById('status').innerText = s 27 | } 28 | 29 | document.getElementById('fetch').addEventListener('click', () => { 30 | console.log('clicked!'); 31 | fetch('/fetch', { 32 | method: 'POST', 33 | credentials: 'include', 34 | headers: { 35 | 'Content-type': 'application/json', 36 | 'x-xsrf-token': '#{csrfToken}' 37 | }, 38 | body: JSON.stringify({ 39 | foo: 42 40 | }) 41 | }) 42 | .then(r => r.json()) 43 | .then(r => r.status) 44 | .then(setStatus) 45 | .then(() => { 46 | console.log('all good') 47 | }, (err) => { 48 | console.error(err) 49 | setStatus('error! ' + err.message) 50 | }) 51 | }); 52 | 53 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title= pageTitle 5 | body 6 | h1 Index page 7 | p Views 8 | div.views #{views} 9 | a(href=`/form`) Visit form page 10 | --------------------------------------------------------------------------------