├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ ├── ci.yml │ └── scorecard.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── scripts └── version-history.js └── test ├── .eslintrc.yml └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - standard 4 | - plugin:markdown/recommended 5 | plugins: 6 | - markdown 7 | overrides: 8 | - files: '**/*.md' 9 | processor: 'markdown/markdown' 10 | rules: 11 | no-param-reassign: error 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | name: 13 | - Node.js 0.10 14 | - Node.js 0.12 15 | - io.js 1.x 16 | - io.js 2.x 17 | - io.js 3.x 18 | - Node.js 4.x 19 | - Node.js 5.x 20 | - Node.js 6.x 21 | - Node.js 7.x 22 | - Node.js 8.x 23 | - Node.js 9.x 24 | - Node.js 10.x 25 | - Node.js 11.x 26 | - Node.js 12.x 27 | - Node.js 13.x 28 | - Node.js 14.x 29 | - Node.js 15.x 30 | - Node.js 16.x 31 | - Node.js 17.x 32 | - Node.js 18.x 33 | - Node.js 19.x 34 | - Node.js 20.x 35 | - Node.js 21.x 36 | 37 | include: 38 | - name: Node.js 0.10 39 | node-version: "0.10" 40 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 41 | 42 | - name: Node.js 0.12 43 | node-version: "0.12" 44 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 45 | 46 | - name: io.js 1.x 47 | node-version: "1.8" 48 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 49 | 50 | - name: io.js 2.x 51 | node-version: "2.5" 52 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 53 | 54 | - name: io.js 3.x 55 | node-version: "3.3" 56 | npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 57 | 58 | - name: Node.js 4.x 59 | node-version: "4.9" 60 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 61 | 62 | - name: Node.js 5.x 63 | node-version: "5.12" 64 | npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 65 | 66 | - name: Node.js 6.x 67 | node-version: "6.17" 68 | npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 69 | 70 | - name: Node.js 7.x 71 | node-version: "7.10" 72 | npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 73 | 74 | - name: Node.js 8.x 75 | node-version: "8.17" 76 | npm-i: mocha@7.2.0 nyc@14.1.1 77 | 78 | - name: Node.js 9.x 79 | node-version: "9.11" 80 | npm-i: mocha@7.2.0 nyc@14.1.1 81 | 82 | - name: Node.js 10.x 83 | node-version: "10.24" 84 | npm-i: mocha@8.4.0 85 | 86 | - name: Node.js 11.x 87 | node-version: "11.15" 88 | npm-i: mocha@8.4.0 89 | 90 | - name: Node.js 12.x 91 | node-version: "12.22" 92 | npm-i: mocha@9.2.2 93 | 94 | - name: Node.js 13.x 95 | node-version: "13.14" 96 | npm-i: mocha@9.2.2 97 | 98 | - name: Node.js 14.x 99 | node-version: "14.21" 100 | 101 | - name: Node.js 15.x 102 | node-version: "15.14" 103 | 104 | - name: Node.js 16.x 105 | node-version: "16.20" 106 | 107 | - name: Node.js 17.x 108 | node-version: "17.9" 109 | 110 | - name: Node.js 18.x 111 | node-version: "18.19" 112 | 113 | - name: Node.js 19.x 114 | node-version: "19.9" 115 | 116 | - name: Node.js 20.x 117 | node-version: "20.11" 118 | 119 | - name: Node.js 21.x 120 | node-version: "21.6" 121 | 122 | steps: 123 | - uses: actions/checkout@v3 124 | 125 | - name: Install Node.js ${{ matrix.node-version }} 126 | shell: bash -eo pipefail -l {0} 127 | run: | 128 | nvm install --default ${{ matrix.node-version }} 129 | dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH" 130 | 131 | - name: Configure npm 132 | run: | 133 | if [[ "$(npm config get package-lock)" == "true" ]]; then 134 | npm config set package-lock false 135 | else 136 | npm config set shrinkwrap false 137 | fi 138 | 139 | - name: Remove npm module(s) ${{ matrix.npm-rm }} 140 | run: npm rm --silent --save-dev ${{ matrix.npm-rm }} 141 | if: matrix.npm-rm != '' 142 | 143 | - name: Install npm module(s) ${{ matrix.npm-i }} 144 | run: npm install --save-dev ${{ matrix.npm-i }} 145 | if: matrix.npm-i != '' 146 | 147 | - name: Setup Node.js version-specific dependencies 148 | shell: bash 149 | run: | 150 | # eslint for linting 151 | # - remove on Node.js < 12 152 | if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then 153 | node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \ 154 | grep -E '^eslint(-|$)' | \ 155 | sort -r | \ 156 | xargs -n1 npm rm --silent --save-dev 157 | fi 158 | 159 | - name: Install Node.js dependencies 160 | run: npm install 161 | 162 | - name: List environment 163 | id: list_env 164 | shell: bash 165 | run: | 166 | echo "node@$(node -v)" 167 | echo "npm@$(npm -v)" 168 | npm -s ls ||: 169 | (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT" 170 | 171 | - name: Run tests 172 | shell: bash 173 | run: | 174 | if npm -ps ls nyc | grep -q nyc; then 175 | npm run test-ci 176 | else 177 | npm test 178 | fi 179 | 180 | - name: Lint code 181 | if: steps.list_env.outputs.eslint != '' 182 | run: npm run lint 183 | 184 | - name: Collect code coverage 185 | uses: coverallsapp/github-action@master 186 | if: steps.list_env.outputs.nyc != '' 187 | with: 188 | github-token: ${{ secrets.GITHUB_TOKEN }} 189 | flag-name: run-${{ matrix.test_number }} 190 | parallel: true 191 | 192 | coverage: 193 | needs: test 194 | runs-on: ubuntu-latest 195 | steps: 196 | - name: Upload code coverage 197 | uses: coverallsapp/github-action@master 198 | with: 199 | github-token: ${{ secrets.github_token }} 200 | parallel-finished: true 201 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | 7 | on: 8 | # For Branch-Protection check. Only the default branch is supported. See 9 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 10 | branch_protection_rule: 11 | # To guarantee Maintained check is occasionally updated. See 12 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 13 | schedule: 14 | - cron: '16 21 * * 1' 15 | push: 16 | branches: [ "master" ] 17 | 18 | # Declare default permissions as read only. 19 | permissions: read-all 20 | 21 | jobs: 22 | analysis: 23 | name: Scorecard analysis 24 | runs-on: ubuntu-latest 25 | permissions: 26 | # Needed to upload the results to code-scanning dashboard. 27 | security-events: write 28 | # Needed to publish results and get a badge (see publish_results below). 29 | id-token: write 30 | # Uncomment the permissions below if installing in a private repository. 31 | # contents: read 32 | # actions: read 33 | 34 | steps: 35 | - name: "Checkout code" 36 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | with: 38 | persist-credentials: false 39 | 40 | - name: "Run analysis" 41 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 42 | with: 43 | results_file: results.sarif 44 | results_format: sarif 45 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 46 | # - you want to enable the Branch-Protection check on a *public* repository, or 47 | # - you are installing Scorecard on a *private* repository 48 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 49 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 50 | 51 | # Public repositories: 52 | # - Publish results to OpenSSF REST API for easy access by consumers 53 | # - Allows the repository to include the Scorecard badge. 54 | # - See https://github.com/ossf/scorecard-action#publishing-results. 55 | # For private repositories: 56 | # - `publish_results` will always be set to `false`, regardless 57 | # of the value entered here. 58 | publish_results: true 59 | 60 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 61 | # format to the repository Actions tab. 62 | - name: "Upload artifact" 63 | uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 64 | with: 65 | name: SARIF file 66 | path: results.sarif 67 | retention-days: 5 68 | 69 | # Upload the results to GitHub's code scanning dashboard. 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 2.1.0 / 2024-01-23 2 | ================== 3 | 4 | * Fix loading sessions with special keys 5 | * deps: cookies@0.9.1 6 | - Add `partitioned` option for CHIPS support 7 | - Add `priority` option for Priority cookie support 8 | - Fix accidental cookie name/value truncation when given invalid chars 9 | - Fix `maxAge` option to reject invalid values 10 | - Remove quotes from returned quoted cookie value 11 | - Use `req.socket` over deprecated `req.connection` 12 | - pref: small lookup regexp optimization 13 | 14 | 2.0.0 / 2021-12-16 15 | ================== 16 | 17 | * deps: debug@3.2.7 18 | * deps: safe-buffer@5.2.1 19 | 20 | 2.0.0-rc.1 / 2020-01-23 21 | ======================= 22 | 23 | * Remove private `req.session.save()` 24 | * Remove undocumented `req.session.length` to free up key name 25 | * Remove undocumented `req.sessionCookies` and `req.sessionKey` 26 | * deps: cookies@0.8.0 27 | - Fix check for default `secure` option behavior 28 | - Fix `maxAge` option preventing cookie deletion 29 | - Support `"none"` in `sameSite` option 30 | - deps: depd@~2.0.0 31 | - deps: keygrip@~1.1.0 32 | - perf: remove argument reassignment 33 | * deps: debug@3.2.6 34 | * deps: on-headers@~1.0.2 35 | - Fix `res.writeHead` patch missing return value 36 | * deps: safe-buffer@5.2.0 37 | * perf: remove internal reference to request from session object 38 | 39 | 2.0.0-beta.3 / 2017-10-13 40 | ========================= 41 | 42 | * deps: cookies@0.7.1 43 | - deps: depd@~1.1.1 44 | - deps: keygrip@~1.0.2 45 | * deps: debug@3.1.0 46 | - Add `DEBUG_HIDE_DATE` 47 | - Add 256 color mode support 48 | - Enable / disable namespaces dynamically 49 | - Make millisecond timer namespace-specific 50 | - Remove `DEBUG_FD` support 51 | - Use `Date#toISOString()` when output is not a TTY 52 | * deps: safe-buffer@5.1.1 53 | 54 | 2.0.0-beta.2 / 2017-05-23 55 | ========================= 56 | 57 | * Create new session for all types of invalid sessions 58 | * Use `safe-buffer` for improved Buffer API 59 | * deps: debug@2.6.8 60 | - Fix `DEBUG_MAX_ARRAY_LENGTH` 61 | - deps: ms@2.0.0 62 | 63 | 2.0.0-beta.1 / 2017-02-19 64 | ========================== 65 | 66 | * Drop support for Node.js 0.8 67 | * deps: cookies@0.7.0 68 | - Add `sameSite` option for SameSite cookie support 69 | - pref: enable strict mode 70 | 71 | 2.0.0-alpha.3 / 2017-02-12 72 | ========================== 73 | 74 | * Use `Object.defineProperty` instead of deprecated `__define*__` 75 | * deps: cookies@0.6.2 76 | - deps: keygrip@~1.0.1 77 | * deps: debug@2.6.1 78 | - Allow colors in workers 79 | - Deprecated `DEBUG_FD` environment variable set to `3` or higher 80 | - Fix error when running under React Native 81 | - Use same color for same namespace 82 | - deps: ms@0.7.2 83 | 84 | 2.0.0-alpha.2 / 2016-11-10 85 | ========================== 86 | 87 | * deps: cookies@0.6.1 88 | * deps: debug@2.3.2 89 | - Fix error when running under React Native 90 | - deps: ms@0.7.2 91 | 92 | 2.0.0-alpha.1 / 2015-10-11 93 | ========================== 94 | 95 | * Change default cookie name to `session` 96 | * Change `.populated` to `.isPopulated` 97 | * Remove the `key` option; use `name` instead 98 | * Save all enumerable properties on `req.session` 99 | - Including `_`-prefixed properties 100 | * perf: reduce the scope of try-catch deopt 101 | * deps: cookies@0.5.1 102 | - Throw on invalid values provided to `Cookie` constructor 103 | * deps: on-headers@~1.0.1 104 | - perf: enable strict mode 105 | 106 | 1.4.0 / 2020-01-06 107 | ================== 108 | 109 | * deps: cookies@0.8.0 110 | - Fix check for default `secure` option behavior 111 | - Fix `maxAge` option preventing cookie deletion 112 | - Support `"none"` in `sameSite` option 113 | - deps: depd@~2.0.0 114 | - deps: keygrip@~1.1.0 115 | 116 | 1.3.3 / 2019-02-28 117 | ================== 118 | 119 | * deps: cookies@0.7.3 120 | - deps: depd@~1.1.2 121 | - deps: keygrip@~1.0.3 122 | - perf: remove argument reassignment 123 | * deps: on-headers@~1.0.2 124 | - Fix `res.writeHead` patch missing return value 125 | 126 | 1.3.2 / 2017-09-24 127 | ================== 128 | 129 | * deps: debug@2.6.9 130 | 131 | 1.3.1 / 2017-08-27 132 | ================== 133 | 134 | * deps: cookies@0.7.1 135 | - deps: depd@~1.1.1 136 | - deps: keygrip@~1.0.2 137 | 138 | 1.3.0 / 2017-08-03 139 | ================== 140 | 141 | * deps: cookies@0.7.0 142 | - Add `sameSite` option for SameSite cookie support 143 | - Throw on invalid values provided to `Cookie` constructor 144 | - deps: keygrip@~1.0.1 145 | - pref: enable strict mode 146 | * deps: debug@2.6.8 147 | - Allow colors in workers 148 | - Deprecate `DEBUG_FD` environment variable set to 3 or higher 149 | - Fix error when running under React Native 150 | - Use same color for same namespace 151 | - deps: ms@2.0.0 152 | * deps: on-headers@~1.0.1 153 | - perf: enable strict mode 154 | 155 | 1.2.0 / 2015-07-01 156 | ================== 157 | 158 | * Make `isNew` non-enumerable and non-writeable 159 | * Make `req.sessionOptions` a shallow clone to override per-request 160 | * deps: debug@~2.2.0 161 | - Fix high intensity foreground color for bold 162 | - deps: ms@0.7.0 163 | * perf: enable strict mode 164 | * perf: remove argument reassignments 165 | 166 | 1.1.0 / 2014-11-09 167 | ================== 168 | 169 | * Fix errors setting cookies to be non-fatal 170 | * Use `on-headers` instead of `writeHead` patching 171 | * deps: cookies@0.5.0 172 | * deps: debug@~2.1.0 173 | 174 | 1.0.2 / 2014-05-07 175 | ================== 176 | 177 | * Add `name` option 178 | 179 | 1.0.1 / 2014-02-24 180 | ================== 181 | 182 | * Fix duplicate `dependencies` in `package.json` 183 | 184 | 1.0.0 / 2014-02-23 185 | ================== 186 | 187 | * Initial release 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Jonathan Ong 4 | Copyright (c) 2014-2017 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cookie-session 2 | 3 | [![NPM Version][npm-version-image]][npm-url] 4 | [![NPM Downloads][npm-downloads-image]][npm-url] 5 | [![Build Status][ci-image]][ci-url] 6 | [![Test Coverage][coveralls-image]][coveralls-url] 7 | 8 | Simple cookie-based session middleware. 9 | 10 | A user session can be stored in two main ways with cookies: on the server or on 11 | the client. This module stores the session data on the client within a cookie, 12 | while a module like [express-session](https://www.npmjs.com/package/express-session) 13 | stores only a session identifier on the client within a cookie and stores the 14 | session data on the server, typically in a database. 15 | 16 | The following points can help you choose which to use: 17 | 18 | * `cookie-session` does not require any database / resources on the server side, 19 | though the total session data cannot exceed the browser's max cookie size. 20 | * `cookie-session` can simplify certain load-balanced scenarios. 21 | * `cookie-session` can be used to store a "light" session and include an identifier 22 | to look up a database-backed secondary store to reduce database lookups. 23 | 24 | **NOTE** This module does not encrypt the session contents in the cookie, only provides 25 | signing to prevent tampering. The client will be able to read the session data by 26 | examining the cookie's value. Secret data should not be set in `req.session` without 27 | encrypting it, or use a server-side session instead. 28 | 29 | **NOTE** This module does not prevent session replay, as the expiration set is that 30 | of the cookie only; if that is a concern of your application, you can store an expiration 31 | date in `req.session` object and validate it on the server, and implement any other logic 32 | to extend the session as your application needs. 33 | 34 | ## Install 35 | 36 | This is a [Node.js](https://nodejs.org/en/) module available through the 37 | [npm registry](https://www.npmjs.com/). Installation is done using the 38 | [`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): 39 | 40 | ```sh 41 | $ npm install cookie-session 42 | ``` 43 | 44 | ## API 45 | 46 | ```js 47 | var cookieSession = require('cookie-session') 48 | var express = require('express') 49 | 50 | var app = express() 51 | 52 | app.use(cookieSession({ 53 | name: 'session', 54 | keys: [/* secret keys */], 55 | 56 | // Cookie Options 57 | maxAge: 24 * 60 * 60 * 1000 // 24 hours 58 | })) 59 | ``` 60 | 61 | ### cookieSession(options) 62 | 63 | Create a new cookie session middleware with the provided options. This middleware 64 | will attach the property `session` to `req`, which provides an object representing 65 | the loaded session. This session is either a new session if no valid session was 66 | provided in the request, or a loaded session from the request. 67 | 68 | The middleware will automatically add a `Set-Cookie` header to the response if the 69 | contents of `req.session` were altered. _Note_ that no `Set-Cookie` header will be 70 | in the response (and thus no session created for a specific user) unless there are 71 | contents in the session, so be sure to add something to `req.session` as soon as 72 | you have identifying information to store for the session. 73 | 74 | #### Options 75 | 76 | Cookie session accepts these properties in the options object. 77 | 78 | ##### name 79 | 80 | The name of the cookie to set, defaults to `session`. 81 | 82 | ##### keys 83 | 84 | The list of keys to use to sign & verify cookie values, or a configured 85 | [`Keygrip`](https://www.npmjs.com/package/keygrip) instance. Set cookies are always 86 | signed with `keys[0]`, while the other keys are valid for verification, allowing 87 | for key rotation. If a `Keygrip` instance is provided, it can be used to 88 | change signature parameters like the algorithm of the signature. 89 | 90 | ##### secret 91 | 92 | A string which will be used as single key if `keys` is not provided. 93 | 94 | ##### Cookie Options 95 | 96 | Other options are passed to `cookies.get()` and `cookies.set()` allowing you 97 | to control security, domain, path, and signing among other settings. 98 | 99 | The options can also contain any of the following (for the full list, see 100 | [cookies module documentation](https://www.npmjs.org/package/cookies#readme): 101 | 102 | - `maxAge`: a number representing the milliseconds from `Date.now()` for expiry 103 | - `expires`: a `Date` object indicating the cookie's expiration date (expires at the end of session by default). 104 | - `path`: a string indicating the path of the cookie (`/` by default). 105 | - `domain`: a string indicating the domain of the cookie (no default). 106 | - `partitioned`: a boolean indicating whether to partition the cookie in Chrome for the [CHIPS Update](https://developers.google.com/privacy-sandbox/3pcd/chips) (`false` by default). If this is true, Cookies from embedded sites will be partitioned and only readable from the same top level site from which it was created. 107 | - `priority`: a string indicating the cookie priority. This can be set to `'low'`, `'medium'`, or `'high'`. 108 | - `sameSite`: a boolean or string indicating whether the cookie is a "same site" cookie (`false` by default). This can be set to `'strict'`, `'lax'`, `'none'`, or `true` (which maps to `'strict'`). 109 | - `secure`: a boolean indicating whether the cookie is only to be sent over HTTPS (`false` by default for HTTP, `true` by default for HTTPS). If this is set to `true` and Node.js is not directly over a TLS connection, be sure to read how to [setup Express behind proxies](https://expressjs.com/en/guide/behind-proxies.html) or the cookie may not ever set correctly. 110 | - `httpOnly`: a boolean indicating whether the cookie is only to be sent over HTTP(S), and not made available to client JavaScript (`true` by default). 111 | - `signed`: a boolean indicating whether the cookie is to be signed (`true` by default). 112 | - `overwrite`: a boolean indicating whether to overwrite previously set cookies of the same name (`true` by default). 113 | 114 | ### req.session 115 | 116 | Represents the session for the given request. 117 | 118 | #### .isChanged 119 | 120 | Is `true` if the session has been changed during the request. 121 | 122 | #### .isNew 123 | 124 | Is `true` if the session is new. 125 | 126 | #### .isPopulated 127 | 128 | Determine if the session has been populated with data or is empty. 129 | 130 | ### req.sessionOptions 131 | 132 | Represents the session options for the current request. These options are a 133 | shallow clone of what was provided at middleware construction and can be 134 | altered to change cookie setting behavior on a per-request basis. 135 | 136 | ### Destroying a session 137 | 138 | To destroy a session simply set it to `null`: 139 | 140 | ```js 141 | req.session = null 142 | ``` 143 | 144 | ### Saving a session 145 | 146 | Since the entire contents of the session is kept in a client-side cookie, the 147 | session is "saved" by writing a cookie out in a `Set-Cookie` response header. 148 | This is done automatically if there has been a change made to the session when 149 | the Node.js response headers are being written to the client and the session 150 | was not destroyed. 151 | 152 | ## Examples 153 | 154 | ### Simple view counter example 155 | 156 | ```js 157 | var cookieSession = require('cookie-session') 158 | var express = require('express') 159 | 160 | var app = express() 161 | 162 | app.set('trust proxy', 1) // trust first proxy 163 | 164 | app.use(cookieSession({ 165 | name: 'session', 166 | keys: ['key1', 'key2'] 167 | })) 168 | 169 | app.get('/', function (req, res, next) { 170 | // Update views 171 | req.session.views = (req.session.views || 0) + 1 172 | 173 | // Write response 174 | res.end(req.session.views + ' views') 175 | }) 176 | 177 | app.listen(3000) 178 | ``` 179 | 180 | ### Per-user sticky max age 181 | 182 | ```js 183 | var cookieSession = require('cookie-session') 184 | var express = require('express') 185 | 186 | var app = express() 187 | 188 | app.set('trust proxy', 1) // trust first proxy 189 | 190 | app.use(cookieSession({ 191 | name: 'session', 192 | keys: ['key1', 'key2'] 193 | })) 194 | 195 | // This allows you to set req.session.maxAge to let certain sessions 196 | // have a different value than the default. 197 | app.use(function (req, res, next) { 198 | req.sessionOptions.maxAge = req.session.maxAge || req.sessionOptions.maxAge 199 | next() 200 | }) 201 | 202 | // ... your logic here ... 203 | ``` 204 | 205 | ### Extending the session expiration 206 | 207 | This module does not send a `Set-Cookie` header if the contents of the session 208 | have not changed. This means that to extend the expiration of a session in the 209 | user's browser (in response to user activity, for example) some kind of 210 | modification to the session needs be made. 211 | 212 | ```js 213 | var cookieSession = require('cookie-session') 214 | var express = require('express') 215 | 216 | var app = express() 217 | 218 | app.use(cookieSession({ 219 | name: 'session', 220 | keys: ['key1', 'key2'] 221 | })) 222 | 223 | // Update a value in the cookie so that the set-cookie will be sent. 224 | // Only changes every minute so that it's not sent with every request. 225 | app.use(function (req, res, next) { 226 | req.session.nowInMinutes = Math.floor(Date.now() / 60e3) 227 | next() 228 | }) 229 | 230 | // ... your logic here ... 231 | ``` 232 | 233 | ### Using a custom signature algorithm 234 | 235 | This example shows creating a custom `Keygrip` instance as the `keys` option 236 | to provide keys and additional signature configuration. 237 | 238 | ```js 239 | var cookieSession = require('cookie-session') 240 | var express = require('express') 241 | var Keygrip = require('keygrip') 242 | 243 | var app = express() 244 | 245 | app.use(cookieSession({ 246 | name: 'session', 247 | keys: new Keygrip(['key1', 'key2'], 'SHA384', 'base64') 248 | })) 249 | 250 | // ... your logic here ... 251 | ``` 252 | 253 | ## Usage Limitations 254 | 255 | ### Max Cookie Size 256 | 257 | Because the entire session object is encoded and stored in a cookie, it is 258 | possible to exceed the maximum cookie size limits on different browsers. The 259 | [RFC6265 specification](https://tools.ietf.org/html/rfc6265#section-6.1) 260 | recommends that a browser **SHOULD** allow 261 | 262 | > At least 4096 bytes per cookie (as measured by the sum of the length of 263 | > the cookie's name, value, and attributes) 264 | 265 | In practice this limit differs slightly across browsers. See a list of 266 | [browser limits here](http://browsercookielimits.iain.guru). As a rule 267 | of thumb **don't exceed 4093 bytes per domain**. 268 | 269 | If your session object is large enough to exceed a browser limit when encoded, 270 | in most cases the browser will refuse to store the cookie. This will cause the 271 | following requests from the browser to either a) not have any session 272 | information or b) use old session information that was small enough to not 273 | exceed the cookie limit. 274 | 275 | If you find your session object is hitting these limits, it is best to 276 | consider if data in your session should be loaded from a database on the 277 | server instead of transmitted to/from the browser with every request. Or 278 | move to an [alternative session strategy](https://github.com/expressjs/session#compatible-session-stores) 279 | 280 | ## License 281 | 282 | [MIT](LICENSE) 283 | 284 | [ci-image]: https://badgen.net/github/checks/expressjs/cookie-session/master?label=ci 285 | [ci-url]: https://github.com/expressjs/cookie-session/actions?query=workflow%3Aci 286 | [coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/cookie-session/master 287 | [coveralls-url]: https://coveralls.io/r/expressjs/cookie-session?branch=master 288 | [npm-downloads-image]: https://badgen.net/npm/dm/cookie-session 289 | [npm-url]: https://npmjs.org/package/cookie-session 290 | [npm-version-image]: https://badgen.net/npm/v/cookie-session 291 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * cookie-session 3 | * Copyright(c) 2013 Jonathan Ong 4 | * Copyright(c) 2014-2017 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | * @private 13 | */ 14 | 15 | var Buffer = require('safe-buffer').Buffer 16 | var debug = require('debug')('cookie-session') 17 | var Cookies = require('cookies') 18 | var onHeaders = require('on-headers') 19 | 20 | /** 21 | * Module exports. 22 | * @public 23 | */ 24 | 25 | module.exports = cookieSession 26 | 27 | /** 28 | * Create a new cookie session middleware. 29 | * 30 | * @param {object} [options] 31 | * @param {boolean} [options.httpOnly=true] 32 | * @param {array} [options.keys] 33 | * @param {string} [options.name=session] Name of the cookie to use 34 | * @param {boolean} [options.overwrite=true] 35 | * @param {string} [options.secret] 36 | * @param {boolean} [options.signed=true] 37 | * @return {function} middleware 38 | * @public 39 | */ 40 | 41 | function cookieSession (options) { 42 | var opts = options || {} 43 | 44 | // cookie name 45 | var name = opts.name || 'session' 46 | 47 | // secrets 48 | var keys = opts.keys 49 | if (!keys && opts.secret) keys = [opts.secret] 50 | 51 | // defaults 52 | if (opts.overwrite == null) opts.overwrite = true 53 | if (opts.httpOnly == null) opts.httpOnly = true 54 | if (opts.signed == null) opts.signed = true 55 | 56 | if (!keys && opts.signed) throw new Error('.keys required.') 57 | 58 | debug('session options %j', opts) 59 | 60 | return function _cookieSession (req, res, next) { 61 | var cookies = new Cookies(req, res, { 62 | keys: keys 63 | }) 64 | var sess 65 | 66 | // for overriding 67 | req.sessionOptions = Object.create(opts) 68 | 69 | // define req.session getter / setter 70 | Object.defineProperty(req, 'session', { 71 | configurable: true, 72 | enumerable: true, 73 | get: getSession, 74 | set: setSession 75 | }) 76 | 77 | function getSession () { 78 | // already retrieved 79 | if (sess) { 80 | return sess 81 | } 82 | 83 | // unset 84 | if (sess === false) { 85 | return null 86 | } 87 | 88 | // get session 89 | if ((sess = tryGetSession(cookies, name, req.sessionOptions))) { 90 | return sess 91 | } 92 | 93 | // create session 94 | debug('new session') 95 | return (sess = Session.create()) 96 | } 97 | 98 | function setSession (val) { 99 | if (val == null) { 100 | // unset session 101 | sess = false 102 | return val 103 | } 104 | 105 | if (typeof val === 'object') { 106 | // create a new session 107 | sess = Session.create(val) 108 | return sess 109 | } 110 | 111 | throw new Error('req.session can only be set as null or an object.') 112 | } 113 | 114 | onHeaders(res, function setHeaders () { 115 | if (sess === undefined) { 116 | // not accessed 117 | return 118 | } 119 | 120 | try { 121 | if (sess === false) { 122 | // remove 123 | debug('remove %s', name) 124 | cookies.set(name, '', req.sessionOptions) 125 | } else if ((!sess.isNew || sess.isPopulated) && sess.isChanged) { 126 | // save populated or non-new changed session 127 | debug('save %s', name) 128 | cookies.set(name, Session.serialize(sess), req.sessionOptions) 129 | } 130 | } catch (e) { 131 | debug('error saving session %s', e.message) 132 | } 133 | }) 134 | 135 | next() 136 | } 137 | }; 138 | 139 | /** 140 | * Session model. 141 | * 142 | * @param {Context} ctx 143 | * @param {Object} obj 144 | * @private 145 | */ 146 | 147 | function Session (ctx, obj) { 148 | Object.defineProperty(this, '_ctx', { 149 | value: ctx 150 | }) 151 | 152 | if (obj) { 153 | for (var key in obj) { 154 | if (!(key in this)) { 155 | this[key] = obj[key] 156 | } 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Create new session. 163 | * @private 164 | */ 165 | 166 | Session.create = function create (obj) { 167 | var ctx = new SessionContext() 168 | return new Session(ctx, obj) 169 | } 170 | 171 | /** 172 | * Create session from serialized form. 173 | * @private 174 | */ 175 | 176 | Session.deserialize = function deserialize (str) { 177 | var ctx = new SessionContext() 178 | var obj = decode(str) 179 | 180 | ctx._new = false 181 | ctx._val = str 182 | 183 | return new Session(ctx, obj) 184 | } 185 | 186 | /** 187 | * Serialize a session to a string. 188 | * @private 189 | */ 190 | 191 | Session.serialize = function serialize (sess) { 192 | return encode(sess) 193 | } 194 | 195 | /** 196 | * Return if the session is changed for this request. 197 | * 198 | * @return {Boolean} 199 | * @public 200 | */ 201 | 202 | Object.defineProperty(Session.prototype, 'isChanged', { 203 | get: function getIsChanged () { 204 | return this._ctx._new || this._ctx._val !== Session.serialize(this) 205 | } 206 | }) 207 | 208 | /** 209 | * Return if the session is new for this request. 210 | * 211 | * @return {Boolean} 212 | * @public 213 | */ 214 | 215 | Object.defineProperty(Session.prototype, 'isNew', { 216 | get: function getIsNew () { 217 | return this._ctx._new 218 | } 219 | }) 220 | 221 | /** 222 | * populated flag, which is just a boolean alias of .length. 223 | * 224 | * @return {Boolean} 225 | * @public 226 | */ 227 | 228 | Object.defineProperty(Session.prototype, 'isPopulated', { 229 | get: function getIsPopulated () { 230 | return Object.keys(this).length > 0 231 | } 232 | }) 233 | 234 | /** 235 | * Session context to store metadata. 236 | * 237 | * @private 238 | */ 239 | 240 | function SessionContext () { 241 | this._new = true 242 | this._val = undefined 243 | } 244 | 245 | /** 246 | * Decode the base64 cookie value to an object. 247 | * 248 | * @param {String} string 249 | * @return {Object} 250 | * @private 251 | */ 252 | 253 | function decode (string) { 254 | var body = Buffer.from(string, 'base64').toString('utf8') 255 | return JSON.parse(body) 256 | } 257 | 258 | /** 259 | * Encode an object into a base64-encoded JSON string. 260 | * 261 | * @param {Object} body 262 | * @return {String} 263 | * @private 264 | */ 265 | 266 | function encode (body) { 267 | var str = JSON.stringify(body) 268 | return Buffer.from(str).toString('base64') 269 | } 270 | 271 | /** 272 | * Try getting a session from a cookie. 273 | * @private 274 | */ 275 | 276 | function tryGetSession (cookies, name, opts) { 277 | var str = cookies.get(name, opts) 278 | 279 | if (!str) { 280 | return undefined 281 | } 282 | 283 | debug('parse %s', str) 284 | 285 | try { 286 | return Session.deserialize(str) 287 | } catch (err) { 288 | return undefined 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cookie-session", 3 | "description": "cookie session middleware", 4 | "version": "2.1.0", 5 | "contributors": [ 6 | "Douglas Christopher Wilson ", 7 | "Jonathan Ong (http://jongleberry.com)" 8 | ], 9 | "license": "MIT", 10 | "keywords": [ 11 | "connect", 12 | "express", 13 | "middleware", 14 | "session" 15 | ], 16 | "repository": "expressjs/cookie-session", 17 | "dependencies": { 18 | "cookies": "0.9.1", 19 | "debug": "3.2.7", 20 | "on-headers": "~1.0.2", 21 | "safe-buffer": "5.2.1" 22 | }, 23 | "devDependencies": { 24 | "connect": "3.7.0", 25 | "eslint": "8.56.0", 26 | "eslint-config-standard": "14.1.1", 27 | "eslint-plugin-import": "2.29.1", 28 | "eslint-plugin-markdown": "3.0.1", 29 | "eslint-plugin-node": "11.1.0", 30 | "eslint-plugin-promise": "6.1.1", 31 | "eslint-plugin-standard": "4.1.0", 32 | "mocha": "10.2.0", 33 | "nyc": "15.1.0", 34 | "supertest": "6.3.4" 35 | }, 36 | "files": [ 37 | "HISTORY.md", 38 | "LICENSE", 39 | "README.md", 40 | "index.js" 41 | ], 42 | "engines": { 43 | "node": ">= 0.10" 44 | }, 45 | "scripts": { 46 | "lint": "eslint .", 47 | "test": "mocha --check-leaks --reporter spec --bail test/", 48 | "test-ci": "nyc --reporter=lcov --reporter=text npm test", 49 | "test-cov": "nyc --reporter=html --reporter=text npm test", 50 | "version": "node scripts/version-history.js && git add HISTORY.md" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/version-history.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | 6 | var HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') 7 | var MD_HEADER_REGEXP = /^====*$/ 8 | var VERSION = process.env.npm_package_version 9 | var VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ 10 | 11 | var historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') 12 | 13 | if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { 14 | console.error('Missing header in HISTORY.md') 15 | process.exit(1) 16 | } 17 | 18 | if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { 19 | console.error('Missing placeholder version in HISTORY.md') 20 | process.exit(1) 21 | } 22 | 23 | if (historyFileLines[0].indexOf('x') !== -1) { 24 | var versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') 25 | 26 | if (!versionCheckRegExp.test(VERSION)) { 27 | console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) 28 | process.exit(1) 29 | } 30 | } 31 | 32 | historyFileLines[0] = VERSION + ' / ' + getLocaleDate() 33 | historyFileLines[1] = repeat('=', historyFileLines[0].length) 34 | 35 | fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) 36 | 37 | function getLocaleDate () { 38 | var now = new Date() 39 | 40 | return zeroPad(now.getFullYear(), 4) + '-' + 41 | zeroPad(now.getMonth() + 1, 2) + '-' + 42 | zeroPad(now.getDate(), 2) 43 | } 44 | 45 | function repeat (str, length) { 46 | var out = '' 47 | 48 | for (var i = 0; i < length; i++) { 49 | out += str 50 | } 51 | 52 | return out 53 | } 54 | 55 | function zeroPad (number, length) { 56 | var num = number.toString() 57 | 58 | while (num.length < length) { 59 | num = '0' + num 60 | } 61 | 62 | return num 63 | } 64 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV = 'test' 3 | 4 | var assert = require('assert') 5 | var connect = require('connect') 6 | var request = require('supertest') 7 | var session = require('..') 8 | 9 | describe('Cookie Session', function () { 10 | describe('"httpOnly" option', function () { 11 | it('should default to "true"', function (done) { 12 | var app = App() 13 | app.use(function (req, res, next) { 14 | req.session.message = 'hi' 15 | res.end(String(req.sessionOptions.httpOnly)) 16 | }) 17 | 18 | request(app) 19 | .get('/') 20 | .expect(shouldHaveCookieWithParameter('session', 'httpOnly')) 21 | .expect(200, 'true', done) 22 | }) 23 | 24 | it('should use given "false"', function (done) { 25 | var app = App({ httpOnly: false }) 26 | app.use(function (req, res, next) { 27 | req.session.message = 'hi' 28 | res.end(String(req.sessionOptions.httpOnly)) 29 | }) 30 | 31 | request(app) 32 | .get('/') 33 | .expect(shouldHaveCookieWithoutParameter('session', 'httpOnly')) 34 | .expect(200, 'false', done) 35 | }) 36 | }) 37 | 38 | describe('"overwrite" option', function () { 39 | it('should default to "true"', function (done) { 40 | var app = App() 41 | app.use(function (req, res, next) { 42 | res.setHeader('Set-Cookie', [ 43 | 'session=foo; path=/fake', 44 | 'foo=bar' 45 | ]) 46 | req.session.message = 'hi' 47 | res.end(String(req.sessionOptions.overwrite)) 48 | }) 49 | 50 | request(app) 51 | .get('/') 52 | .expect(shouldHaveCookieWithValue('foo', 'bar')) 53 | .expect(200, 'true', done) 54 | }) 55 | 56 | it('should use given "false"', function (done) { 57 | var app = App({ overwrite: false }) 58 | app.use(function (req, res, next) { 59 | res.setHeader('Set-Cookie', [ 60 | 'session=foo; path=/fake', 61 | 'foo=bar' 62 | ]) 63 | req.session.message = 'hi' 64 | res.end(String(req.sessionOptions.overwrite)) 65 | }) 66 | 67 | request(app) 68 | .get('/') 69 | .expect(shouldHaveCookieWithValue('foo', 'bar')) 70 | .expect('Set-Cookie', /session=foo/) 71 | .expect(200, 'false', done) 72 | }) 73 | }) 74 | 75 | describe('when options.name = my.session', function () { 76 | it('should use my.session for cookie name', function (done) { 77 | var app = App({ name: 'my.session' }) 78 | app.use(function (req, res, next) { 79 | req.session.message = 'hi' 80 | res.end() 81 | }) 82 | 83 | request(app) 84 | .get('/') 85 | .expect(shouldHaveCookie('my.session')) 86 | .expect(200, done) 87 | }) 88 | }) 89 | 90 | describe('when options.signed = true', function () { 91 | describe('when options.keys are set', function () { 92 | before(function () { 93 | this.app = connect() 94 | this.app.use(session({ keys: ['a', 'b'] })) 95 | this.app.use('/get', function (req, res) { 96 | res.setHeader('Content-Type', 'application/json') 97 | res.end(JSON.stringify(req.session)) 98 | }) 99 | this.app.use('/set', function (req, res) { 100 | req.session.message = 'hi' 101 | res.end() 102 | }) 103 | }) 104 | 105 | it('should set cookie signature', function (done) { 106 | request(this.app) 107 | .get('/set') 108 | .expect(shouldHaveCookie('session')) 109 | .expect(shouldHaveCookie('session.sig')) 110 | .expect(200, '', done) 111 | }) 112 | 113 | it('should set cookie signature with first key', function (done) { 114 | request(this.app) 115 | .get('/set') 116 | .expect(shouldHaveCookieWithValue('session', 'eyJtZXNzYWdlIjoiaGkifQ==')) 117 | .expect(shouldHaveCookieWithValue('session.sig', 'vdp2-kj-91tgzbWcV1QzofT3hu0')) 118 | .expect(200, '', done) 119 | }) 120 | 121 | it('should accept session with signature', function (done) { 122 | request(this.app) 123 | .get('/get') 124 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==; session.sig=vdp2-kj-91tgzbWcV1QzofT3hu0') 125 | .expect(200, { message: 'hi' }, done) 126 | }) 127 | 128 | it('should accept session with secondary signature', function (done) { 129 | request(this.app) 130 | .get('/get') 131 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==; session.sig=SiRRAEncekXEzVdvey_7SkWaMM4') 132 | .expect(200, { message: 'hi' }, done) 133 | }) 134 | 135 | it('should reject session with invalid signature', function (done) { 136 | request(this.app) 137 | .get('/get') 138 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==; session.sig=foobar') 139 | .expect(200, {}, done) 140 | }) 141 | 142 | it('should reject session with no signature', function (done) { 143 | request(this.app) 144 | .get('/get') 145 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==') 146 | .expect(200, {}, done) 147 | }) 148 | }) 149 | 150 | describe('when options.secret is set', function () { 151 | before(function () { 152 | this.app = connect() 153 | this.app.use(session({ secret: 'a' })) 154 | this.app.use('/get', function (req, res) { 155 | res.setHeader('Content-Type', 'application/json') 156 | res.end(JSON.stringify(req.session)) 157 | }) 158 | this.app.use('/set', function (req, res) { 159 | req.session.message = 'hi' 160 | res.end() 161 | }) 162 | }) 163 | 164 | it('should set cookie signature', function (done) { 165 | request(this.app) 166 | .get('/set') 167 | .expect(shouldHaveCookie('session')) 168 | .expect(shouldHaveCookie('session.sig')) 169 | .expect(200, '', done) 170 | }) 171 | 172 | it('should set cookie signature with only key', function (done) { 173 | request(this.app) 174 | .get('/set') 175 | .expect(shouldHaveCookieWithValue('session', 'eyJtZXNzYWdlIjoiaGkifQ==')) 176 | .expect(shouldHaveCookieWithValue('session.sig', 'vdp2-kj-91tgzbWcV1QzofT3hu0')) 177 | .expect(200, '', done) 178 | }) 179 | 180 | it('should accept session with signature', function (done) { 181 | request(this.app) 182 | .get('/get') 183 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==; session.sig=vdp2-kj-91tgzbWcV1QzofT3hu0') 184 | .expect(200, { message: 'hi' }, done) 185 | }) 186 | 187 | it('should reject session with invalid signature', function (done) { 188 | request(this.app) 189 | .get('/get') 190 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==; session.sig=foobar') 191 | .expect(200, {}, done) 192 | }) 193 | 194 | it('should reject session with no signature', function (done) { 195 | request(this.app) 196 | .get('/get') 197 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==') 198 | .expect(200, {}, done) 199 | }) 200 | }) 201 | 202 | describe('when options.keys are not set', function () { 203 | it('should throw', function () { 204 | assert.throws(function () { 205 | session() 206 | }, /\.keys required/) 207 | }) 208 | }) 209 | }) 210 | 211 | describe('when options.signed = false', function () { 212 | before(function () { 213 | this.app = connect() 214 | this.app.use(session({ signed: false })) 215 | this.app.use('/get', function (req, res) { 216 | res.setHeader('Content-Type', 'application/json') 217 | res.end(JSON.stringify(req.session)) 218 | }) 219 | this.app.use('/set', function (req, res) { 220 | req.session.message = 'hi' 221 | res.end() 222 | }) 223 | }) 224 | 225 | it('should not set cookie signature', function (done) { 226 | request(this.app) 227 | .get('/set') 228 | .expect(shouldHaveCookie('session')) 229 | .expect(shouldNotHaveCookie('session.sig')) 230 | .expect(200, done) 231 | }) 232 | 233 | it('should accept session without signature', function (done) { 234 | request(this.app) 235 | .get('/get') 236 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==') 237 | .expect(200, { message: 'hi' }, done) 238 | }) 239 | 240 | it('should accept session with invalid signature', function (done) { 241 | request(this.app) 242 | .get('/get') 243 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==; session.sig=foobar') 244 | .expect(200, { message: 'hi' }, done) 245 | }) 246 | }) 247 | 248 | describe('when options.secure = true', function () { 249 | describe('when connection not secured', function () { 250 | it('should not Set-Cookie', function (done) { 251 | var app = App({ secure: true }) 252 | app.use(function (req, res, next) { 253 | process.nextTick(function () { 254 | req.session.message = 'hello!' 255 | res.end('greetings') 256 | }) 257 | }) 258 | 259 | request(app) 260 | .get('/') 261 | .expect(shouldNotSetCookies()) 262 | .expect(200, done) 263 | }) 264 | }) 265 | }) 266 | 267 | describe('when the session contains a ;', function () { 268 | it('should still work', function (done) { 269 | var app = App() 270 | app.use(function (req, res, next) { 271 | if (req.method === 'POST') { 272 | req.session.string = ';' 273 | res.statusCode = 204 274 | res.end() 275 | } else { 276 | res.end(req.session.string) 277 | } 278 | }) 279 | 280 | request(app) 281 | .post('/') 282 | .expect(shouldHaveCookie('session')) 283 | .expect(204, function (err, res) { 284 | if (err) return done(err) 285 | request(app) 286 | .get('/') 287 | .set('Cookie', cookieHeader(cookies(res))) 288 | .expect(';', done) 289 | }) 290 | }) 291 | }) 292 | 293 | describe('when the session is invalid', function () { 294 | it('should create new session', function (done) { 295 | var app = App({ name: 'my.session', signed: false }) 296 | app.use(function (req, res, next) { 297 | res.end(String(req.session.isNew)) 298 | }) 299 | 300 | request(app) 301 | .get('/') 302 | .set('Cookie', 'my.session=bogus') 303 | .expect(200, 'true', done) 304 | }) 305 | }) 306 | 307 | describe('new session', function () { 308 | describe('when not accessed', function () { 309 | it('should not Set-Cookie', function (done) { 310 | var app = App() 311 | app.use(function (req, res, next) { 312 | res.end('greetings') 313 | }) 314 | 315 | request(app) 316 | .get('/') 317 | .expect(shouldNotSetCookies()) 318 | .expect(200, done) 319 | }) 320 | }) 321 | 322 | describe('when accessed and not populated', function (done) { 323 | it('should not Set-Cookie', function (done) { 324 | var app = App() 325 | app.use(function (req, res, next) { 326 | var sess = req.session 327 | res.end(JSON.stringify(sess)) 328 | }) 329 | 330 | request(app) 331 | .get('/') 332 | .expect(shouldNotSetCookies()) 333 | .expect(200, done) 334 | }) 335 | }) 336 | 337 | describe('when populated', function (done) { 338 | it('should Set-Cookie', function (done) { 339 | var app = App() 340 | app.use(function (req, res, next) { 341 | req.session.message = 'hello' 342 | res.end() 343 | }) 344 | 345 | request(app) 346 | .get('/') 347 | .expect(shouldHaveCookie('session')) 348 | .expect(200, done) 349 | }) 350 | }) 351 | }) 352 | 353 | describe('saved session', function () { 354 | var cookie 355 | 356 | before(function (done) { 357 | var app = App() 358 | app.use(function (req, res, next) { 359 | req.session.message = 'hello' 360 | res.end() 361 | }) 362 | 363 | request(app) 364 | .get('/') 365 | .expect(shouldHaveCookie('session')) 366 | .expect(200, function (err, res) { 367 | if (err) return done(err) 368 | cookie = cookieHeader(cookies(res)) 369 | done() 370 | }) 371 | }) 372 | 373 | describe('when not accessed', function () { 374 | it('should not Set-Cookie', function (done) { 375 | var app = App() 376 | app.use(function (req, res, next) { 377 | res.end('aklsjdfklasjdf') 378 | }) 379 | 380 | request(app) 381 | .get('/') 382 | .set('Cookie', cookie) 383 | .expect(shouldNotSetCookies()) 384 | .expect(200, done) 385 | }) 386 | }) 387 | 388 | describe('when accessed but not changed', function () { 389 | it('should be the same session', function (done) { 390 | var app = App() 391 | app.use(function (req, res, next) { 392 | assert.strictEqual(req.session.message, 'hello') 393 | res.end('aklsjdfkljasdf') 394 | }) 395 | 396 | request(app) 397 | .get('/') 398 | .set('Cookie', cookie) 399 | .expect(200, done) 400 | }) 401 | 402 | it('should not Set-Cookie', function (done) { 403 | var app = App() 404 | app.use(function (req, res, next) { 405 | assert.strictEqual(req.session.message, 'hello') 406 | res.end('aklsjdfkljasdf') 407 | }) 408 | 409 | request(app) 410 | .get('/') 411 | .set('Cookie', cookie) 412 | .expect(shouldNotSetCookies()) 413 | .expect(200, done) 414 | }) 415 | }) 416 | 417 | describe('when accessed and changed', function () { 418 | it('should Set-Cookie', function (done) { 419 | var app = App() 420 | app.use(function (req, res, next) { 421 | req.session.money = '$$$' 422 | res.end('klajsdlkfjadsf') 423 | }) 424 | 425 | request(app) 426 | .get('/') 427 | .set('Cookie', cookie) 428 | .expect(shouldHaveCookie('session')) 429 | .expect(200, done) 430 | }) 431 | }) 432 | }) 433 | 434 | describe('when session = ', function () { 435 | describe('null', function () { 436 | it('should expire the session', function (done) { 437 | var app = App() 438 | app.use(function (req, res, next) { 439 | req.session = null 440 | res.end('lkajsdf') 441 | }) 442 | 443 | request(app) 444 | .get('/') 445 | .expect(shouldHaveCookie('session')) 446 | .expect(200, done) 447 | }) 448 | 449 | it('should no longer return a session', function (done) { 450 | var app = App() 451 | app.use(function (req, res, next) { 452 | req.session = null 453 | res.end(JSON.stringify(req.session)) 454 | }) 455 | 456 | request(app) 457 | .get('/') 458 | .expect(shouldHaveCookie('session')) 459 | .expect(200, 'null', done) 460 | }) 461 | }) 462 | 463 | describe('{}', function () { 464 | it('should not Set-Cookie', function (done) { 465 | var app = App() 466 | app.use(function (req, res, next) { 467 | req.session = {} 468 | res.end('hello, world') 469 | }) 470 | 471 | request(app) 472 | .get('/') 473 | .expect(shouldNotSetCookies()) 474 | .expect(200, 'hello, world', done) 475 | }) 476 | }) 477 | 478 | describe('{a: b}', function () { 479 | it('should create a session', function (done) { 480 | var app = App() 481 | app.use(function (req, res, next) { 482 | req.session = { message: 'hello', foo: 'bar', isChanged: true } 483 | res.end('klajsdfasdf') 484 | }) 485 | 486 | request(app) 487 | .get('/') 488 | .expect(shouldHaveCookie('session')) 489 | .expect(200, done) 490 | }) 491 | 492 | it('should not error on special properties', function (done) { 493 | var app = App() 494 | app.use(function (req, res) { 495 | req.session = { message: 'hello', isChanged: false } 496 | res.end() 497 | }) 498 | 499 | request(app) 500 | .get('/') 501 | .expect(shouldHaveCookie('session')) 502 | .expect(200, done) 503 | }) 504 | }) 505 | 506 | describe('anything else', function () { 507 | it('should throw', function (done) { 508 | var app = App() 509 | app.use(function (req, res, next) { 510 | req.session = 'aklsdjfasdf' 511 | }) 512 | 513 | request(app) 514 | .get('/') 515 | .expect(500, done) 516 | }) 517 | }) 518 | }) 519 | 520 | describe('req.session', function () { 521 | describe('.isPopulated', function () { 522 | it('should be false on new session', function (done) { 523 | var app = App() 524 | app.use(function (req, res, next) { 525 | res.end(String(req.session.isPopulated)) 526 | }) 527 | 528 | request(app) 529 | .get('/') 530 | .expect(200, 'false', done) 531 | }) 532 | 533 | it('should be true after adding property', function (done) { 534 | var app = App() 535 | app.use(function (req, res, next) { 536 | req.session.message = 'hello!' 537 | res.end(String(req.session.isPopulated)) 538 | }) 539 | 540 | request(app) 541 | .get('/') 542 | .expect(200, 'true', done) 543 | }) 544 | 545 | it('should be true loading session', function (done) { 546 | var app = App({ signed: false }) 547 | app.use(function (req, res) { 548 | res.end(String(req.session.isPopulated)) 549 | }) 550 | 551 | request(app) 552 | .get('/') 553 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkifQ==') 554 | .expect(200, 'true', done) 555 | }) 556 | 557 | it('should not conflict with session value', function (done) { 558 | var app = App({ signed: false }) 559 | app.use(function (req, res) { 560 | res.end(String(req.session.isPopulated)) 561 | }) 562 | 563 | request(app) 564 | .get('/') 565 | .set('Cookie', 'session=eyJtZXNzYWdlIjoiaGkiLCJpc1BvcHVsYXRlZCI6ZmFsc2V9') 566 | .expect(200, 'true', done) 567 | }) 568 | }) 569 | }) 570 | 571 | describe('req.sessionOptions', function () { 572 | it('should be the session options', function (done) { 573 | var app = App({ name: 'my.session' }) 574 | app.use(function (req, res, next) { 575 | res.end(String(req.sessionOptions.name)) 576 | }) 577 | 578 | request(app) 579 | .get('/') 580 | .expect(200, 'my.session', done) 581 | }) 582 | 583 | it('should alter the cookie setting', function (done) { 584 | var app = App({ maxAge: 3600000, name: 'my.session' }) 585 | app.use(function (req, res, next) { 586 | if (req.url === '/max') { 587 | req.sessionOptions.maxAge = 6500000 588 | } 589 | 590 | req.session.message = 'hello!' 591 | res.end() 592 | }) 593 | 594 | request(app) 595 | .get('/') 596 | .expect(shouldHaveCookieWithTTLBetween('my.session', 0, 3600000)) 597 | .expect(200, function (err) { 598 | if (err) return done(err) 599 | request(app) 600 | .get('/max') 601 | .expect(shouldHaveCookieWithTTLBetween('my.session', 5000000, Infinity)) 602 | .expect(200, done) 603 | }) 604 | }) 605 | }) 606 | }) 607 | 608 | function App (options) { 609 | var opts = Object.create(options || null) 610 | opts.keys = ['a', 'b'] 611 | var app = connect() 612 | app.use(session(opts)) 613 | return app 614 | } 615 | 616 | function cookieHeader (cookies) { 617 | return Object.keys(cookies).map(function (name) { 618 | return name + '=' + cookies[name].value 619 | }).join('; ') 620 | } 621 | 622 | function cookies (res) { 623 | var headers = res.headers['set-cookie'] || [] 624 | var obj = Object.create(null) 625 | 626 | for (var i = 0; i < headers.length; i++) { 627 | var params = Object.create(null) 628 | var parts = headers[i].split(';') 629 | var nvp = parts[0].split('=') 630 | 631 | for (var j = 1; j < parts.length; j++) { 632 | var pvp = parts[j].split('=') 633 | 634 | params[pvp[0].trim().toLowerCase()] = pvp[1] 635 | ? pvp[1].trim() 636 | : true 637 | } 638 | 639 | var ttl = params.expires 640 | ? Date.parse(params.expires) - Date.parse(res.headers.date) 641 | : null 642 | 643 | obj[nvp[0].trim()] = { 644 | value: nvp.slice(1).join('=').trim(), 645 | params: params, 646 | ttl: ttl 647 | } 648 | } 649 | 650 | return obj 651 | } 652 | 653 | function shouldHaveCookie (name) { 654 | return function (res) { 655 | assert.ok((name in cookies(res)), 'should have cookie "' + name + '"') 656 | } 657 | } 658 | 659 | function shouldHaveCookieWithParameter (name, param) { 660 | return function (res) { 661 | assert.ok((name in cookies(res)), 'should have cookie "' + name + '"') 662 | assert.ok((param.toLowerCase() in cookies(res)[name].params), 663 | 'should have parameter "' + param + '"') 664 | } 665 | } 666 | 667 | function shouldHaveCookieWithoutParameter (name, param) { 668 | return function (res) { 669 | assert.ok((name in cookies(res)), 'should have cookie "' + name + '"') 670 | assert.ok(!(param.toLowerCase() in cookies(res)[name].params), 671 | 'should not have parameter "' + param + '"') 672 | } 673 | } 674 | 675 | function shouldHaveCookieWithTTLBetween (name, low, high) { 676 | return function (res) { 677 | assert.ok((name in cookies(res)), 'should have cookie "' + name + '"') 678 | assert.ok(('expires' in cookies(res)[name].params), 679 | 'should have parameter "expires"') 680 | assert.ok((cookies(res)[name].ttl >= low && cookies(res)[name].ttl <= high), 681 | 'should have TTL between ' + low + ' and ' + high) 682 | } 683 | } 684 | 685 | function shouldHaveCookieWithValue (name, value) { 686 | return function (res) { 687 | assert.ok((name in cookies(res)), 'should have cookie "' + name + '"') 688 | assert.strictEqual(cookies(res)[name].value, value) 689 | } 690 | } 691 | 692 | function shouldNotHaveCookie (name) { 693 | return function (res) { 694 | assert.ok(!(name in cookies(res)), 'should not have cookie "' + name + '"') 695 | } 696 | } 697 | 698 | function shouldNotSetCookies () { 699 | return function (res) { 700 | assert.strictEqual(res.headers['set-cookie'], undefined, 'should not set cookies') 701 | } 702 | } 703 | --------------------------------------------------------------------------------