diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..cdb36c1b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[{*.js,*.json,*.yml}]
+indent_size = 2
+indent_style = space
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..76b1021d
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+.nyc_output
+coverage
+node_modules
diff --git a/.eslintrc.yml b/.eslintrc.yml
new file mode 100644
index 00000000..b6b9f62f
--- /dev/null
+++ b/.eslintrc.yml
@@ -0,0 +1,15 @@
+root: true
+extends:
+ - plugin:markdown/recommended
+plugins:
+ - markdown
+overrides:
+ - files: '**/*.md'
+ processor: 'markdown/markdown'
+rules:
+ eol-last: error
+ eqeqeq: ["error", "always", { "null": "ignore" }]
+ indent: ["error", 2, { "MemberExpression": "off", "SwitchCase": 1 }]
+ no-mixed-spaces-and-tabs: error
+ no-trailing-spaces: error
+ one-var: ["error", { "initialized": "never" }]
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..7d2c7033
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ groups:
+ github-actions:
+ patterns:
+ - "*"
+
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: monthly
+ ignore:
+ - dependency-name: "*"
+ update-types: ["version-update:semver-major"]
+ groups:
+ npm-dependencies:
+ patterns:
+ - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..004c35ed
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,264 @@
+name: ci
+
+on:
+- pull_request
+- push
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ name:
+ - Node.js 0.8
+ - Node.js 0.10
+ - Node.js 0.12
+ - io.js 1.x
+ - io.js 2.x
+ - io.js 3.x
+ - Node.js 4.x
+ - Node.js 5.x
+ - Node.js 6.x
+ - Node.js 7.x
+ - Node.js 8.x
+ - Node.js 9.x
+ - Node.js 10.x
+ - Node.js 11.x
+ - Node.js 12.x
+ - Node.js 13.x
+ - Node.js 14.x
+ - Node.js 15.x
+ - Node.js 16.x
+ - Node.js 17.x
+ - Node.js 18.x
+ - Node.js 19.x
+ - Node.js 20.x
+ - Node.js 21.x
+ - Node.js 22.x
+ - Node.js 23.x
+ - Node.js 24.x
+ - Node.js 25.x
+
+ include:
+ - name: Node.js 0.8
+ node-version: "0.8"
+ npm-i: mocha@2.5.3 supertest@1.1.0
+ npm-rm: nyc
+
+ - name: Node.js 0.10
+ node-version: "0.10"
+ npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0
+
+ - name: Node.js 0.12
+ node-version: "0.12"
+ npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0
+
+ - name: io.js 1.x
+ node-version: "1.8"
+ npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0
+
+ - name: io.js 2.x
+ node-version: "2.5"
+ npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0
+
+ - name: io.js 3.x
+ node-version: "3.3"
+ npm-i: mocha@2.5.3 nyc@10.3.2 supertest@2.0.0
+
+ - name: Node.js 4.x
+ node-version: "4.9"
+ npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2
+
+ - name: Node.js 5.x
+ node-version: "5.12"
+ npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2
+
+ - name: Node.js 6.x
+ node-version: "6.17"
+ npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6
+
+ - name: Node.js 7.x
+ node-version: "7.10"
+ npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6
+
+ - name: Node.js 8.x
+ node-version: "8.17"
+ npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6
+
+ - name: Node.js 9.x
+ node-version: "9.11"
+ npm-i: mocha@7.2.0 nyc@14.1.1 supertest@6.1.6
+
+ - name: Node.js 10.x
+ node-version: "10.24"
+ npm-i: mocha@8.4.0 supertest@6.1.6
+
+ - name: Node.js 11.x
+ node-version: "11.15"
+ npm-i: mocha@8.4.0 supertest@6.1.6
+
+ - name: Node.js 12.x
+ node-version: "12.22"
+ npm-i: mocha@9.2.2 supertest@6.1.6
+
+ - name: Node.js 13.x
+ node-version: "13.14"
+ npm-i: mocha@9.2.2 supertest@6.1.6
+
+ - name: Node.js 14.x
+ node-version: "14.21"
+
+ - name: Node.js 15.x
+ node-version: "15.14"
+ npm-i: supertest@6.1.6
+
+ - name: Node.js 16.x
+ node-version: "16.20"
+
+ - name: Node.js 17.x
+ node-version: "17.9"
+
+ - name: Node.js 18.x
+ node-version: "18.19"
+
+ - name: Node.js 19.x
+ node-version: "19.9"
+
+ - name: Node.js 20.x
+ node-version: "20"
+
+ - name: Node.js 21.x
+ node-version: "21"
+
+ - name: Node.js 22.x
+ node-version: "22"
+
+ - name: Node.js 23.x
+ node-version: "23"
+
+ - name: Node.js 24.x
+ node-version: "24"
+
+ - name: Node.js 25.x
+ node-version: "25"
+
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+
+ - name: Install Node.js ${{ matrix.node-version }}
+ shell: bash -eo pipefail -l {0}
+ run: |
+ nvm install --default ${{ matrix.node-version }}
+ if [[ "${{ matrix.node-version }}" == 0.* && "$(cut -d. -f2 <<< "${{ matrix.node-version }}")" -lt 10 ]]; then
+ nvm install --alias=npm 0.10
+ nvm use ${{ matrix.node-version }}
+ if [[ "$(npm -v)" == 1.1.* ]]; then
+ nvm exec npm npm install -g npm@1.1
+ ln -fs "$(which npm)" "$(dirname "$(nvm which npm)")/npm"
+ else
+ sed -i '1s;^.*$;'"$(printf '#!%q' "$(nvm which npm)")"';' "$(readlink -f "$(which npm)")"
+ fi
+ npm config set strict-ssl false
+ fi
+ dirname "$(nvm which ${{ matrix.node-version }})" >> "$GITHUB_PATH"
+
+ - name: Configure npm
+ run: |
+ if [[ "$(npm config get package-lock)" == "true" ]]; then
+ npm config set package-lock false
+ else
+ npm config set shrinkwrap false
+ fi
+
+ - name: Remove npm module(s) ${{ matrix.npm-rm }}
+ run: npm rm --silent --save-dev ${{ matrix.npm-rm }}
+ if: matrix.npm-rm != ''
+
+ - name: Install npm module(s) ${{ matrix.npm-i }}
+ run: npm install --save-dev ${{ matrix.npm-i }}
+ if: matrix.npm-i != ''
+
+ - name: Setup Node.js version-specific dependencies
+ shell: bash
+ run: |
+ # eslint for linting
+ # - remove on Node.js < 12
+ if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -lt 12 ]]; then
+ node -pe 'Object.keys(require("./package").devDependencies).join("\n")' | \
+ grep -E '^eslint(-|$)' | \
+ sort -r | \
+ xargs -n1 npm rm --silent --save-dev
+ fi
+
+ - name: Install Node.js dependencies
+ run: npm install
+
+ - name: List environment
+ id: list_env
+ shell: bash
+ run: |
+ echo "node@$(node -v)"
+ echo "npm@$(npm -v)"
+ npm -s ls ||:
+ (npm -s ls --depth=0 ||:) | awk -F'[ @]' 'NR>1 && $2 { print $2 "=" $3 }' >> "$GITHUB_OUTPUT"
+
+ - name: Run tests
+ shell: bash
+ run: |
+ if npm -ps ls nyc | grep -q nyc; then
+ npm run test-ci
+ cp coverage/lcov.info "coverage/${{ matrix.node-version }}.lcov"
+ else
+ npm test
+ fi
+
+ - name: Lint code
+ if: steps.list_env.outputs.eslint != ''
+ run: npm run lint
+
+ - name: Collect code coverage
+ if: steps.list_env.outputs.nyc != ''
+ run: |
+ if [[ -d ./coverage ]]; then
+ mv ./coverage "./${{ matrix.node-version }}"
+ mkdir ./coverage
+ mv "./${{ matrix.node-version }}" "./coverage/${{ matrix.node-version }}"
+ fi
+
+ - name: Upload code coverage
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ if: steps.list_env.outputs.nyc != ''
+ with:
+ name: coverage-${{ matrix.node-version }}
+ path: "./coverage/${{ matrix.node-version }}"
+ retention-days: 1
+
+ coverage:
+ permissions:
+ checks: write # for coverallsapp/github-action to create new checks
+ contents: read # for actions/checkout to fetch code
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+
+ - name: Install lcov
+ shell: bash
+ run: sudo apt-get -y install lcov
+
+ - name: Collect coverage reports
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ path: ./coverage
+
+ - name: Merge coverage reports
+ shell: bash
+ run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./coverage/lcov.info
+
+ - name: Upload coverage report
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # master
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 00000000..139ec706
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,66 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: ["master"]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: ["master"]
+ schedule:
+ - cron: "0 0 * * 1"
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
+ with:
+ languages: javascript
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ # - name: Autobuild
+ # uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # v3.24.7
+
+ # âšī¸ Command-line programs to run using the OS shell.
+ # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
+ with:
+ category: "/language:javascript"
\ No newline at end of file
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 00000000..2bb15b23
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,73 @@
+# This workflow uses actions that are not certified by GitHub. They are provided
+# by a third-party and are governed by separate terms of service, privacy
+# policy, and support documentation.
+
+name: Scorecard supply-chain security
+
+on:
+ # For Branch-Protection check. Only the default branch is supported. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+ branch_protection_rule:
+ # To guarantee Maintained check is occasionally updated. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+ schedule:
+ - cron: '16 21 * * 1'
+ push:
+ branches: [ "master" ]
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ # Needed to upload the results to code-scanning dashboard.
+ security-events: write
+ # Needed to publish results and get a badge (see publish_results below).
+ id-token: write
+ # Uncomment the permissions below if installing in a private repository.
+ # contents: read
+ # actions: read
+
+ steps:
+ - name: "Checkout code"
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
+
+ - name: "Run analysis"
+ uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
+ # - you want to enable the Branch-Protection check on a *public* repository, or
+ # - you are installing Scorecard on a *private* repository
+ # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
+ # repo_token: ${{ secrets.SCORECARD_TOKEN }}
+
+ # Public repositories:
+ # - Publish results to OpenSSF REST API for easy access by consumers
+ # - Allows the repository to include the Scorecard badge.
+ # - See https://github.com/ossf/scorecard-action#publishing-results.
+ # For private repositories:
+ # - `publish_results` will always be set to `false`, regardless
+ # of the value entered here.
+ publish_results: true
+
+ # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
+ # format to the repository Actions tab.
+ - name: "Upload artifact"
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: SARIF file
+ path: results.sarif
+ retention-days: 5
+
+ # Upload the results to GitHub's code scanning dashboard.
+ - name: "Upload to code-scanning"
+ uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
+ with:
+ sarif_file: results.sarif
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index df9af16b..1b6fef23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+.nyc_output
coverage
node_modules
npm-debug.log
+package-lock.json
+/test/fixtures/server.crt
+/test/fixtures/server.key
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 388f282d..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-language: node_js
-node_js:
- - "0.8"
- - "0.10"
- - "0.12"
- - "1.8"
- - "2.5"
- - "3.3"
- - "4.4"
- - "5.11"
-sudo: false
-cache:
- directories:
- - node_modules
-before_install:
- # Setup Node.js version-specific dependencies
- - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul"
- # Update Node.js modules
- - "test ! -d node_modules || npm prune"
- - "test ! -d node_modules || npm rebuild"
-script:
- # Run test script, depending on istanbul install
- - "test ! -z $(npm -ps ls istanbul) || npm test"
- - "test -z $(npm -ps ls istanbul) || npm run-script test-travis"
-after_script:
- - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls"
diff --git a/HISTORY.md b/HISTORY.md
index 8ec88855..251d5fa7 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,3 +1,178 @@
+unreleased
+==========
+
+ * Enforce RFC 6265 section 5.1.4 path matching for the session cookie path
+ * Fix `secure: 'auto'` and `sameSite: 'auto'` not being resolved when `cookie` is a function
+
+1.19.0 / 2026-01-22
+==========
+
+ * Add dynamic cookie options support
+ * Add sameSite 'auto' support for automatic SameSite attribute configuration
+ * deps: use tilde notation for dependencies
+
+1.18.2 / 2025-07-17
+==========
+ * deps: mocha@10.8.2
+ * deps: on-headers@~1.1.0
+ - Fix [CVE-2025-7339](https://www.cve.org/CVERecord?id=CVE-2025-7339) ([GHSA-76c9-3jph-rj3q](https://github.com/expressjs/on-headers/security/advisories/GHSA-76c9-3jph-rj3q))
+
+1.18.1 / 2024-10-08
+==========
+
+ * deps: cookie@0.7.2
+ - Fix object assignment of `hasOwnProperty`
+ * deps: cookie@0.7.1
+ - Allow leading dot for domain
+ - Although not permitted in the spec, some users expect this to work and user agents ignore the leading dot according to spec
+ - Add fast path for `serialize` without options, use `obj.hasOwnProperty` when parsing
+ * deps: cookie@0.7.0
+ - perf: parse cookies ~10% faster
+ - fix: narrow the validation of cookies to match RFC6265
+ - fix: add `main` to `package.json` for rspack
+
+1.18.0 / 2024-01-28
+===================
+
+ * Add debug log for pathname mismatch
+ * Add `partitioned` to `cookie` options
+ * Add `priority` to `cookie` options
+ * Fix handling errors from setting cookie
+ * Support any type in `secret` that `crypto.createHmac` supports
+ * deps: cookie@0.6.0
+ - Fix `expires` option to reject invalid dates
+ - perf: improve default decode speed
+ - perf: remove slow string split in parse
+ * deps: cookie-signature@1.0.7
+
+1.17.3 / 2022-05-11
+===================
+
+ * Fix resaving already-saved new session at end of request
+ * deps: cookie@0.4.2
+
+1.17.2 / 2021-05-19
+===================
+
+ * Fix `res.end` patch to always commit headers
+ * deps: cookie@0.4.1
+ * deps: safe-buffer@5.2.1
+
+1.17.1 / 2020-04-16
+===================
+
+ * Fix internal method wrapping error on failed reloads
+
+1.17.0 / 2019-10-10
+===================
+
+ * deps: cookie@0.4.0
+ - Add `SameSite=None` support
+ * deps: safe-buffer@5.2.0
+
+1.16.2 / 2019-06-12
+===================
+
+ * Fix restoring `cookie.originalMaxAge` when store returns `Date`
+ * deps: parseurl@~1.3.3
+
+1.16.1 / 2019-04-11
+===================
+
+ * Fix error passing `data` option to `Cookie` constructor
+ * Fix uncaught error from bad session data
+
+1.16.0 / 2019-04-10
+===================
+
+ * Catch invalid `cookie.maxAge` value earlier
+ * Deprecate setting `cookie.maxAge` to a `Date` object
+ * Fix issue where `resave: false` may not save altered sessions
+ * Remove `utils-merge` dependency
+ * Use `safe-buffer` for improved Buffer API
+ * Use `Set-Cookie` as cookie header name for compatibility
+ * deps: depd@~2.0.0
+ - Replace internal `eval` usage with `Function` constructor
+ - Use instance methods on `process` to check for listeners
+ - perf: remove argument reassignment
+ * deps: on-headers@~1.0.2
+ - Fix `res.writeHead` patch missing return value
+
+1.15.6 / 2017-09-26
+===================
+
+ * deps: debug@2.6.9
+ * deps: parseurl@~1.3.2
+ - perf: reduce overhead for full URLs
+ - perf: unroll the "fast-path" `RegExp`
+ * deps: uid-safe@~2.1.5
+ - perf: remove only trailing `=`
+ * deps: utils-merge@1.0.1
+
+1.15.5 / 2017-08-02
+===================
+
+ * Fix `TypeError` when `req.url` is an empty string
+ * deps: depd@~1.1.1
+ - Remove unnecessary `Buffer` loading
+
+1.15.4 / 2017-07-18
+===================
+
+ * deps: debug@2.6.8
+
+1.15.3 / 2017-05-17
+===================
+
+ * deps: debug@2.6.7
+ - deps: ms@2.0.0
+
+1.15.2 / 2017-03-26
+===================
+
+ * deps: debug@2.6.3
+ - Fix `DEBUG_MAX_ARRAY_LENGTH`
+ * deps: uid-safe@~2.1.4
+ - Remove `base64-url` dependency
+
+1.15.1 / 2017-02-10
+===================
+
+ * deps: debug@2.6.1
+ - Fix deprecation messages in WebStorm and other editors
+ - Undeprecate `DEBUG_FD` set to `1` or `2`
+
+1.15.0 / 2017-01-22
+===================
+
+ * Fix detecting modified session when session contains "cookie" property
+ * Fix resaving already-saved reloaded session at end of request
+ * deps: crc@3.4.4
+ - perf: use `Buffer.from` when available
+ * deps: debug@2.6.0
+ - Allow colors in workers
+ - Deprecated `DEBUG_FD` environment variable
+ - Use same color for same namespace
+ - Fix error when running under React Native
+ - deps: ms@0.7.2
+ * perf: remove unreachable branch in set-cookie method
+
+1.14.2 / 2016-10-30
+===================
+
+ * deps: crc@3.4.1
+ - Fix deprecation warning in Node.js 7.x
+ * deps: uid-safe@~2.1.3
+ - deps: base64-url@1.3.3
+
+1.14.1 / 2016-08-24
+===================
+
+ * Fix not always resetting session max age before session save
+ * Fix the cookie `sameSite` option to actually alter the `Set-Cookie`
+ * deps: uid-safe@~2.1.2
+ - deps: base64-url@1.3.2
+
1.14.0 / 2016-07-01
===================
@@ -10,7 +185,7 @@
- Improve error message when `expires` is not a `Date`
- perf: enable strict mode
- perf: use for loop in parse
- - perf: use string concatination for serialization
+ - perf: use string concatenation for serialization
* deps: parseurl@~1.3.1
- perf: enable strict mode
* deps: uid-safe@~2.1.1
diff --git a/README.md b/README.md
index b88ed824..162ea14d 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,17 @@
# express-session
-[![NPM Version][npm-image]][npm-url]
-[![NPM Downloads][downloads-image]][downloads-url]
-[![Build Status][travis-image]][travis-url]
+[![NPM Version][npm-version-image]][npm-url]
+[![NPM Downloads][npm-downloads-image]][node-url]
+[![Build Status][ci-image]][ci-url]
[![Test Coverage][coveralls-image]][coveralls-url]
-[![Gratipay][gratipay-image]][gratipay-url]
## Installation
-```bash
+This is a [Node.js](https://nodejs.org/en/) module available through the
+[npm registry](https://www.npmjs.com/). Installation is done using the
+[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally):
+
+```sh
$ npm install express-session
```
@@ -46,15 +49,35 @@ For a list of stores, see [compatible session stores](#compatible-session-stores
Settings object for the session ID cookie. The default value is
`{ path: '/', httpOnly: true, secure: false, maxAge: null }`.
+In addition to providing a static object, you can also pass a callback function to dynamically generate the cookie options for each request. The callback receives the `req` object as its argument and should return an object containing the cookie settings.
+
+```js
+var app = express()
+app.use(session({
+ secret: 'keyboard cat',
+ resave: false,
+ saveUninitialized: true,
+ cookie: function(req) {
+ var match = req.url.match(/^\/([^/]+)/);
+ return {
+ path: match ? '/' + match[1] : '/',
+ httpOnly: true,
+ secure: req.secure || false,
+ maxAge: 60000
+ }
+ }
+}))
+```
+
The following are options that can be set in this object.
-###### domain
+##### cookie.domain
Specifies the value for the `Domain` `Set-Cookie` attribute. By default, no domain
is set, and most clients will consider the cookie to apply to only the current
domain.
-###### expires
+##### cookie.expires
Specifies the `Date` object to be the value for the `Expires` `Set-Cookie` attribute.
By default, no expiration is set, and most clients will consider this a
@@ -67,7 +90,7 @@ defined in the object is what is used.
**Note** The `expires` option should not be set directly; instead only use the `maxAge`
option.
-###### httpOnly
+##### cookie.httpOnly
Specifies the `boolean` value for the `HttpOnly` `Set-Cookie` attribute. When truthy,
the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly`
@@ -76,7 +99,7 @@ attribute is set.
**Note** be careful when setting this to `true`, as compliant clients will not allow
client-side JavaScript to see the cookie in `document.cookie`.
-###### maxAge
+##### cookie.maxAge
Specifies the `number` (in milliseconds) to use when calculating the `Expires`
`Set-Cookie` attribute. This is done by taking the current server time and adding
@@ -86,27 +109,74 @@ no maximum age is set.
**Note** If both `expires` and `maxAge` are set in the options, then the last one
defined in the object is what is used.
-###### path
+##### cookie.partitioned
+
+Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies)
+attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not.
+By default, the `Partitioned` attribute is not set.
+
+**Note** This is an attribute that has not yet been fully standardized, and may
+change in the future. This also means many clients may ignore this attribute until
+they understand it.
+
+More information about can be found in [the proposal](https://github.com/privacycg/CHIPS).
+
+##### cookie.path
Specifies the value for the `Path` `Set-Cookie`. By default, this is set to `'/'`, which
is the root path of the domain.
-###### sameSite
+Since 1.19.1, path matching follows [RFC 6265 section 5.1.4][rfc-6265-5.1.4]. This means
+the session middleware will only activate when the request path is an exact match or falls
+under a segment boundary of the cookie path. For example, a cookie path of `/admin` will
+match `/admin` and `/admin/users` but will **not** match `/administrator`. Prior versions
+used a simple prefix check that did not enforce segment boundaries.
+
+##### cookie.priority
+
+Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
+
+ - `'low'` will set the `Priority` attribute to `Low`.
+ - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
+ - `'high'` will set the `Priority` attribute to `High`.
+
+More information about the different priority levels can be found in
+[the specification][rfc-west-cookie-priority-00-4.1].
+
+**Note** This is an attribute that has not yet been fully standardized, and may change in the future.
+This also means many clients may ignore this attribute until they understand it.
+
+##### cookie.sameSite
Specifies the `boolean` or `string` to be the value for the `SameSite` `Set-Cookie` attribute.
+By default, this is `false`.
- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
- `false` will not set the `SameSite` attribute.
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
+ - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
+ - `'auto'` will set the `SameSite` attribute to `None` for secure connections and `Lax` for non-secure connections.
-More information about the different enforcement levels can be found in the specification
-https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1.1
+More information about the different enforcement levels can be found in
+[the specification][rfc-6265bis-03-4.1.2.7].
**Note** This is an attribute that has not yet been fully standardized, and may change in
the future. This also means many clients may ignore this attribute until they understand it.
-###### secure
+**Note** There is a [draft spec](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01)
+that requires that the `Secure` attribute be set to `true` when the `SameSite` attribute has been
+set to `'none'`. Some web browsers or other clients may be adopting this specification.
+
+The `cookie.sameSite` option can also be set to the special value `'auto'` to have
+this setting automatically match the determined security of the connection. When the connection
+is secure (HTTPS), the `SameSite` attribute will be set to `None` to enable cross-site usage.
+When the connection is not secure (HTTP), the `SameSite` attribute will be set to `Lax` for
+better security while maintaining functionality. This is useful when the Express `"trust proxy"`
+setting is properly setup to simplify development vs production configuration, particularly
+for SAML authentication scenarios.
+
+##### cookie.secure
Specifies the `boolean` value for the `Secure` `Set-Cookie` attribute. When truthy,
the `Secure` attribute is set, otherwise it is not. By default, the `Secure`
@@ -224,15 +294,24 @@ likely need `resave: true`.
##### rolling
-Force a session identifier cookie to be set on every response. The expiration
+Force the session identifier cookie to be set on every response. The expiration
is reset to the original [`maxAge`](#cookiemaxage), resetting the expiration
countdown.
The default value is `false`.
+With this enabled, the session identifier cookie will expire in
+[`maxAge`](#cookiemaxage) since the last response was sent instead of in
+[`maxAge`](#cookiemaxage) since the session was last modified by the server.
+
+This is typically used in conjunction with short, non-session-length
+[`maxAge`](#cookiemaxage) values to provide a quick timeout of the session data
+with reduced potential of it occurring during on going server interactions.
+
**Note** When this option is set to `true` but the `saveUninitialized` option is
set to `false`, the cookie will not be set on a response with an uninitialized
-session.
+session. This option only modifies the behavior when an existing session was
+loaded for the request.
##### saveUninitialized
@@ -256,10 +335,29 @@ it to be saved. *This has been fixed in PassportJS 0.3.0*
**Required option**
-This is the secret used to sign the session ID cookie. This can be either a string
-for a single secret, or an array of multiple secrets. If an array of secrets is
-provided, only the first element will be used to sign the session ID cookie, while
-all the elements will be considered when verifying the signature in requests.
+This is the secret used to sign the session ID cookie. The secret can be any type
+of value that is supported by Node.js `crypto.createHmac` (like a string or a
+`Buffer`). This can be either a single secret, or an array of multiple secrets. If
+an array of secrets is provided, only the first element will be used to sign the
+session ID cookie, while all the elements will be considered when verifying the
+signature in requests. The secret itself should be not easily parsed by a human and
+would best be a random set of characters. A best practice may include:
+
+ - The use of environment variables to store the secret, ensuring the secret itself
+ does not exist in your repository.
+ - Periodic updates of the secret, while ensuring the previous secret is in the
+ array.
+
+Using a secret that cannot be guessed will reduce the ability to hijack a session to
+only guessing the session ID (as determined by the `genid` option).
+
+Changing the secret value will invalidate all existing sessions. In order to rotate
+the secret without invalidating sessions, provide an array of secrets, with the new
+secret as first element of the array, and including previous secrets as the later
+elements.
+
+**Note** HMAC-256 is used to sign the session ID. For this reason, the secret should
+contain at least 32 bytes of entropy.
##### store
@@ -288,24 +386,24 @@ app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
// Access the session as req.session
app.get('/', function(req, res, next) {
- var sess = req.session
- if (sess.views) {
- sess.views++
+ if (req.session.views) {
+ req.session.views++
res.setHeader('Content-Type', 'text/html')
- res.write('
views: ' + sess.views + '
')
- res.write('expires in: ' + (sess.cookie.maxAge / 1000) + 's
')
+ res.write('views: ' + req.session.views + '
')
+ res.write('expires in: ' + (req.session.cookie.maxAge / 1000) + 's
')
res.end()
} else {
- sess.views = 1
+ req.session.views = 1
res.end('welcome to the session demo. refresh!')
}
})
```
-#### Session.regenerate()
+#### Session.regenerate(callback)
To regenerate the session simply invoke the method. Once complete,
-a new SID and `Session` instance will be initialized at `req.session`.
+a new SID and `Session` instance will be initialized at `req.session`
+and the `callback` will be invoked.
```js
req.session.regenerate(function(err) {
@@ -313,9 +411,10 @@ req.session.regenerate(function(err) {
})
```
-#### Session.destroy()
+#### Session.destroy(callback)
-Destroys the session, removing `req.session`; will be re-generated next request.
+Destroys the session and will unset the `req.session` property.
+Once complete, the `callback` will be invoked.
```js
req.session.destroy(function(err) {
@@ -323,9 +422,10 @@ req.session.destroy(function(err) {
})
```
-#### Session.reload()
+#### Session.reload(callback)
-Reloads the session data.
+Reloads the session data from the store and re-populates the
+`req.session` object. Once complete, the `callback` will be invoked.
```js
req.session.reload(function(err) {
@@ -333,7 +433,7 @@ req.session.reload(function(err) {
})
```
-#### Session.save()
+#### Session.save(callback)
Save the session back to the store, replacing the contents on the store with the
contents in memory (though a store may do something else--consult the store's
@@ -344,8 +444,8 @@ session data has been altered (though this behavior can be altered with various
options in the middleware constructor). Because of this, typically this method
does not need to be called.
-There are some cases where it is useful to call this method, for example, long-
-lived requests or in WebSockets.
+There are some cases where it is useful to call this method, for example,
+redirects, long-lived requests or in WebSockets.
```js
req.session.save(function(err) {
@@ -360,8 +460,10 @@ not necessary to call, as the session middleware does this for you.
### req.session.id
-Each session has a unique ID associated with it. This property will
-contain the session ID and cannot be modified.
+Each session has a unique ID associated with it. This property is an
+alias of [`req.sessionID`](#reqsessionid-1) and cannot be modified.
+It has been added to make the session ID accessible from the `session`
+object.
### req.session.cookie
@@ -385,13 +487,18 @@ req.session.cookie.maxAge = hour
For example when `maxAge` is set to `60000` (one minute), and 30 seconds
has elapsed it will return `30000` until the current request has completed,
-at which time `req.session.touch()` is called to reset `req.session.maxAge`
-to its original value.
+at which time `req.session.touch()` is called to reset
+`req.session.cookie.maxAge` to its original value.
```js
req.session.cookie.maxAge // => 30000
```
+#### Cookie.originalMaxAge
+
+The `req.session.cookie.originalMaxAge` property returns the original
+`maxAge` (time-to-live), in milliseconds, of the session cookie.
+
### req.sessionID
To get the ID of the loaded session, access the request property
@@ -477,127 +584,324 @@ potentially resetting the idle timer.
The following modules implement a session store that is compatible with this
module. Please make a PR to add additional modules :)
+[![â
][aerospike-session-store-image] aerospike-session-store][aerospike-session-store-url] A session store using [Aerospike](http://www.aerospike.com/).
+
+[aerospike-session-store-url]: https://www.npmjs.com/package/aerospike-session-store
+[aerospike-session-store-image]: https://badgen.net/github/stars/aerospike/aerospike-session-store-expressjs?label=%E2%98%85
+
+[![â
][better-sqlite3-session-store-image] better-sqlite3-session-store][better-sqlite3-session-store-url] A session store based on [better-sqlite3](https://github.com/JoshuaWise/better-sqlite3).
+
+[better-sqlite3-session-store-url]: https://www.npmjs.com/package/better-sqlite3-session-store
+[better-sqlite3-session-store-image]: https://badgen.net/github/stars/timdaub/better-sqlite3-session-store?label=%E2%98%85
+
[![â
][cassandra-store-image] cassandra-store][cassandra-store-url] An Apache Cassandra-based session store.
[cassandra-store-url]: https://www.npmjs.com/package/cassandra-store
-[cassandra-store-image]: https://img.shields.io/github/stars/webcc/cassandra-store.svg?label=%E2%98%85
+[cassandra-store-image]: https://badgen.net/github/stars/webcc/cassandra-store?label=%E2%98%85
[![â
][cluster-store-image] cluster-store][cluster-store-url] A wrapper for using in-process / embedded
stores - such as SQLite (via knex), leveldb, files, or memory - with node cluster (desirable for Raspberry Pi 2
and other multi-core embedded devices).
[cluster-store-url]: https://www.npmjs.com/package/cluster-store
-[cluster-store-image]: https://img.shields.io/github/stars/coolaj86/cluster-store.svg?label=%E2%98%85
+[cluster-store-image]: https://badgen.net/github/stars/coolaj86/cluster-store?label=%E2%98%85
+
+[![â
][connect-arango-image] connect-arango][connect-arango-url] An ArangoDB-based session store.
+
+[connect-arango-url]: https://www.npmjs.com/package/connect-arango
+[connect-arango-image]: https://badgen.net/github/stars/AlexanderArvidsson/connect-arango?label=%E2%98%85
[![â
][connect-azuretables-image] connect-azuretables][connect-azuretables-url] An [Azure Table Storage](https://azure.microsoft.com/en-gb/services/storage/tables/)-based session store.
[connect-azuretables-url]: https://www.npmjs.com/package/connect-azuretables
-[connect-azuretables-image]: https://img.shields.io/github/stars/mike-goodwin/connect-azuretables.svg?label=%E2%98%85
+[connect-azuretables-image]: https://badgen.net/github/stars/mike-goodwin/connect-azuretables?label=%E2%98%85
+
+[![â
][connect-cloudant-store-image] connect-cloudant-store][connect-cloudant-store-url] An [IBM Cloudant](https://cloudant.com/)-based session store.
+
+[connect-cloudant-store-url]: https://www.npmjs.com/package/connect-cloudant-store
+[connect-cloudant-store-image]: https://badgen.net/github/stars/adriantanasa/connect-cloudant-store?label=%E2%98%85
+
+[![â
][connect-cosmosdb-image] connect-cosmosdb][connect-cosmosdb-url] An Azure [Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db/)-based session store.
+
+[connect-cosmosdb-url]: https://www.npmjs.com/package/connect-cosmosdb
+[connect-cosmosdb-image]: https://badgen.net/github/stars/thekillingspree/connect-cosmosdb?label=%E2%98%85
[![â
][connect-couchbase-image] connect-couchbase][connect-couchbase-url] A [couchbase](http://www.couchbase.com/)-based session store.
[connect-couchbase-url]: https://www.npmjs.com/package/connect-couchbase
-[connect-couchbase-image]: https://img.shields.io/github/stars/christophermina/connect-couchbase.svg?label=%E2%98%85
+[connect-couchbase-image]: https://badgen.net/github/stars/christophermina/connect-couchbase?label=%E2%98%85
+
+[![â
][connect-datacache-image] connect-datacache][connect-datacache-url] An [IBM Bluemix Data Cache](http://www.ibm.com/cloud-computing/bluemix/)-based session store.
+
+[connect-datacache-url]: https://www.npmjs.com/package/connect-datacache
+[connect-datacache-image]: https://badgen.net/github/stars/adriantanasa/connect-datacache?label=%E2%98%85
+
+[![â
][@google-cloud/connect-datastore-image] @google-cloud/connect-datastore][@google-cloud/connect-datastore-url] A [Google Cloud Datastore](https://cloud.google.com/datastore/docs/concepts/overview)-based session store.
+
+[@google-cloud/connect-datastore-url]: https://www.npmjs.com/package/@google-cloud/connect-datastore
+[@google-cloud/connect-datastore-image]: https://badgen.net/github/stars/GoogleCloudPlatform/cloud-datastore-session-node?label=%E2%98%85
+
+[![â
][connect-db2-image] connect-db2][connect-db2-url] An IBM DB2-based session store built using [ibm_db](https://www.npmjs.com/package/ibm_db) module.
+
+[connect-db2-url]: https://www.npmjs.com/package/connect-db2
+[connect-db2-image]: https://badgen.net/github/stars/wallali/connect-db2?label=%E2%98%85
[![â
][connect-dynamodb-image] connect-dynamodb][connect-dynamodb-url] A DynamoDB-based session store.
-[connect-dynamodb-url]: https://github.com/ca98am79/connect-dynamodb
-[connect-dynamodb-image]: https://img.shields.io/github/stars/ca98am79/connect-dynamodb.svg?label=%E2%98%85
+[connect-dynamodb-url]: https://www.npmjs.com/package/connect-dynamodb
+[connect-dynamodb-image]: https://badgen.net/github/stars/ca98am79/connect-dynamodb?label=%E2%98%85
+
+[![â
][@google-cloud/connect-firestore-image] @google-cloud/connect-firestore][@google-cloud/connect-firestore-url] A [Google Cloud Firestore](https://cloud.google.com/firestore/docs/overview)-based session store.
+
+[@google-cloud/connect-firestore-url]: https://www.npmjs.com/package/@google-cloud/connect-firestore
+[@google-cloud/connect-firestore-image]: https://badgen.net/github/stars/googleapis/nodejs-firestore-session?label=%E2%98%85
+
+[![â
][connect-hazelcast-image] connect-hazelcast][connect-hazelcast-url] Hazelcast session store for Connect and Express.
+
+[connect-hazelcast-url]: https://www.npmjs.com/package/connect-hazelcast
+[connect-hazelcast-image]: https://badgen.net/github/stars/huseyinbabal/connect-hazelcast?label=%E2%98%85
+
+[![â
][connect-loki-image] connect-loki][connect-loki-url] A Loki.js-based session store.
+
+[connect-loki-url]: https://www.npmjs.com/package/connect-loki
+[connect-loki-image]: https://badgen.net/github/stars/Requarks/connect-loki?label=%E2%98%85
+
+[![â
][connect-lowdb-image] connect-lowdb][connect-lowdb-url] A lowdb-based session store.
+
+[connect-lowdb-url]: https://www.npmjs.com/package/connect-lowdb
+[connect-lowdb-image]: https://badgen.net/github/stars/travishorn/connect-lowdb?label=%E2%98%85
+
+[![â
][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store.
+
+[connect-memcached-url]: https://www.npmjs.com/package/connect-memcached
+[connect-memcached-image]: https://badgen.net/github/stars/balor/connect-memcached?label=%E2%98%85
+
+[![â
][connect-memjs-image] connect-memjs][connect-memjs-url] A memcached-based session store using
+[memjs](https://www.npmjs.com/package/memjs) as the memcached client.
+
+[connect-memjs-url]: https://www.npmjs.com/package/connect-memjs
+[connect-memjs-image]: https://badgen.net/github/stars/liamdon/connect-memjs?label=%E2%98%85
-[![â
][connect-mssql-image] connect-mssql][connect-mssql-url] A SQL Server-based session store.
+[![â
][connect-ml-image] connect-ml][connect-ml-url] A MarkLogic Server-based session store.
-[connect-mssql-url]: https://www.npmjs.com/package/connect-mssql
-[connect-mssql-image]: https://img.shields.io/github/stars/patriksimek/connect-mssql.svg?label=%E2%98%85
+[connect-ml-url]: https://www.npmjs.com/package/connect-ml
+[connect-ml-image]: https://badgen.net/github/stars/bluetorch/connect-ml?label=%E2%98%85
[![â
][connect-monetdb-image] connect-monetdb][connect-monetdb-url] A MonetDB-based session store.
[connect-monetdb-url]: https://www.npmjs.com/package/connect-monetdb
-[connect-monetdb-image]: https://img.shields.io/github/stars/MonetDB/npm-connect-monetdb.svg?label=%E2%98%85
+[connect-monetdb-image]: https://badgen.net/github/stars/MonetDB/npm-connect-monetdb?label=%E2%98%85
[![â
][connect-mongo-image] connect-mongo][connect-mongo-url] A MongoDB-based session store.
[connect-mongo-url]: https://www.npmjs.com/package/connect-mongo
-[connect-mongo-image]: https://img.shields.io/github/stars/kcbanner/connect-mongo.svg?label=%E2%98%85
+[connect-mongo-image]: https://badgen.net/github/stars/kcbanner/connect-mongo?label=%E2%98%85
[![â
][connect-mongodb-session-image] connect-mongodb-session][connect-mongodb-session-url] Lightweight MongoDB-based session store built and maintained by MongoDB.
[connect-mongodb-session-url]: https://www.npmjs.com/package/connect-mongodb-session
-[connect-mongodb-session-image]: https://img.shields.io/github/stars/mongodb-js/connect-mongodb-session.svg?label=%E2%98%85
+[connect-mongodb-session-image]: https://badgen.net/github/stars/mongodb-js/connect-mongodb-session?label=%E2%98%85
+
+[![â
][connect-mssql-v2-image] connect-mssql-v2][connect-mssql-v2-url] A Microsoft SQL Server-based session store based on [connect-mssql](https://www.npmjs.com/package/connect-mssql).
+
+[connect-mssql-v2-url]: https://www.npmjs.com/package/connect-mssql-v2
+[connect-mssql-v2-image]: https://badgen.net/github/stars/jluboff/connect-mssql-v2?label=%E2%98%85
+
+[![â
][connect-neo4j-image] connect-neo4j][connect-neo4j-url] A [Neo4j](https://neo4j.com)-based session store.
+
+[connect-neo4j-url]: https://www.npmjs.com/package/connect-neo4j
+[connect-neo4j-image]: https://badgen.net/github/stars/MaxAndersson/connect-neo4j?label=%E2%98%85
+
+[![â
][connect-ottoman-image] connect-ottoman][connect-ottoman-url] A [couchbase ottoman](http://www.couchbase.com/)-based session store.
+
+[connect-ottoman-url]: https://www.npmjs.com/package/connect-ottoman
+[connect-ottoman-image]: https://badgen.net/github/stars/noiissyboy/connect-ottoman?label=%E2%98%85
[![â
][connect-pg-simple-image] connect-pg-simple][connect-pg-simple-url] A PostgreSQL-based session store.
[connect-pg-simple-url]: https://www.npmjs.com/package/connect-pg-simple
-[connect-pg-simple-image]: https://img.shields.io/github/stars/voxpelli/node-connect-pg-simple.svg?label=%E2%98%85
+[connect-pg-simple-image]: https://badgen.net/github/stars/voxpelli/node-connect-pg-simple?label=%E2%98%85
[![â
][connect-redis-image] connect-redis][connect-redis-url] A Redis-based session store.
[connect-redis-url]: https://www.npmjs.com/package/connect-redis
-[connect-redis-image]: https://img.shields.io/github/stars/tj/connect-redis.svg?label=%E2%98%85
+[connect-redis-image]: https://badgen.net/github/stars/tj/connect-redis?label=%E2%98%85
-[![â
][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store.
+[![â
][connect-session-firebase-image] connect-session-firebase][connect-session-firebase-url] A session store based on the [Firebase Realtime Database](https://firebase.google.com/docs/database/)
-[connect-memcached-url]: https://www.npmjs.com/package/connect-memcached
-[connect-memcached-image]: https://img.shields.io/github/stars/balor/connect-memcached.svg?label=%E2%98%85
+[connect-session-firebase-url]: https://www.npmjs.com/package/connect-session-firebase
+[connect-session-firebase-image]: https://badgen.net/github/stars/benweier/connect-session-firebase?label=%E2%98%85
[![â
][connect-session-knex-image] connect-session-knex][connect-session-knex-url] A session store using
[Knex.js](http://knexjs.org/), which is a SQL query builder for PostgreSQL, MySQL, MariaDB, SQLite3, and Oracle.
[connect-session-knex-url]: https://www.npmjs.com/package/connect-session-knex
-[connect-session-knex-image]: https://img.shields.io/github/stars/llambda/connect-session-knex.svg?label=%E2%98%85
+[connect-session-knex-image]: https://badgen.net/github/stars/llambda/connect-session-knex?label=%E2%98%85
[![â
][connect-session-sequelize-image] connect-session-sequelize][connect-session-sequelize-url] A session store using
-[Sequelize.js](http://sequelizejs.com/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL.
+[Sequelize.js](https://sequelize.org/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL.
[connect-session-sequelize-url]: https://www.npmjs.com/package/connect-session-sequelize
-[connect-session-sequelize-image]: https://img.shields.io/github/stars/mweibel/connect-session-sequelize.svg?label=%E2%98%85
+[connect-session-sequelize-image]: https://badgen.net/github/stars/mweibel/connect-session-sequelize?label=%E2%98%85
+
+[![â
][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store.
+
+[connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3
+[connect-sqlite3-image]: https://badgen.net/github/stars/rawberg/connect-sqlite3?label=%E2%98%85
+
+[![â
][connect-typeorm-image] connect-typeorm][connect-typeorm-url] A [TypeORM](https://github.com/typeorm/typeorm)-based session store.
+
+[connect-typeorm-url]: https://www.npmjs.com/package/connect-typeorm
+[connect-typeorm-image]: https://badgen.net/github/stars/makepost/connect-typeorm?label=%E2%98%85
+
+[![â
][couchdb-expression-image] couchdb-expression][couchdb-expression-url] A [CouchDB](https://couchdb.apache.org/)-based session store.
+
+[couchdb-expression-url]: https://www.npmjs.com/package/couchdb-expression
+[couchdb-expression-image]: https://badgen.net/github/stars/tkshnwesper/couchdb-expression?label=%E2%98%85
+
+[![â
][dynamodb-store-image] dynamodb-store][dynamodb-store-url] A DynamoDB-based session store.
+
+[dynamodb-store-url]: https://www.npmjs.com/package/dynamodb-store
+[dynamodb-store-image]: https://badgen.net/github/stars/rafaelrpinto/dynamodb-store?label=%E2%98%85
+
+[![â
][express-etcd-image] express-etcd][express-etcd-url] An [etcd](https://github.com/stianeikeland/node-etcd) based session store.
+
+[express-etcd-url]: https://www.npmjs.com/package/express-etcd
+[express-etcd-image]: https://badgen.net/github/stars/gildean/express-etcd?label=%E2%98%85
[![â
][express-mysql-session-image] express-mysql-session][express-mysql-session-url] A session store using native
[MySQL](https://www.mysql.com/) via the [node-mysql](https://github.com/felixge/node-mysql) module.
[express-mysql-session-url]: https://www.npmjs.com/package/express-mysql-session
-[express-mysql-session-image]: https://img.shields.io/github/stars/chill117/express-mysql-session.svg?label=%E2%98%85
-
-[![â
][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store.
-
-[connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3
-[connect-sqlite3-image]: https://img.shields.io/github/stars/rawberg/connect-sqlite3.svg?label=%E2%98%85
+[express-mysql-session-image]: https://badgen.net/github/stars/chill117/express-mysql-session?label=%E2%98%85
[![â
][express-nedb-session-image] express-nedb-session][express-nedb-session-url] A NeDB-based session store.
[express-nedb-session-url]: https://www.npmjs.com/package/express-nedb-session
-[express-nedb-session-image]: https://img.shields.io/github/stars/louischatriot/express-nedb-session.svg?label=%E2%98%85
+[express-nedb-session-image]: https://badgen.net/github/stars/louischatriot/express-nedb-session?label=%E2%98%85
+
+[![â
][express-oracle-session-image] express-oracle-session][express-oracle-session-url] A session store using native
+[oracle](https://www.oracle.com/) via the [node-oracledb](https://www.npmjs.com/package/oracledb) module.
+
+[express-oracle-session-url]: https://www.npmjs.com/package/express-oracle-session
+[express-oracle-session-image]: https://badgen.net/github/stars/slumber86/express-oracle-session?label=%E2%98%85
+
+[![â
][express-session-cache-manager-image] express-session-cache-manager][express-session-cache-manager-url]
+A store that implements [cache-manager](https://www.npmjs.com/package/cache-manager), which supports
+a [variety of storage types](https://www.npmjs.com/package/cache-manager#store-engines).
+
+[express-session-cache-manager-url]: https://www.npmjs.com/package/express-session-cache-manager
+[express-session-cache-manager-image]: https://badgen.net/github/stars/theogravity/express-session-cache-manager?label=%E2%98%85
+
+[![â
][express-session-etcd3-image] express-session-etcd3][express-session-etcd3-url] An [etcd3](https://github.com/mixer/etcd3) based session store.
+
+[express-session-etcd3-url]: https://www.npmjs.com/package/express-session-etcd3
+[express-session-etcd3-image]: https://badgen.net/github/stars/willgm/express-session-etcd3?label=%E2%98%85
+
+[![â
][express-session-level-image] express-session-level][express-session-level-url] A [LevelDB](https://github.com/Level/levelup) based session store.
+
+[express-session-level-url]: https://www.npmjs.com/package/express-session-level
+[express-session-level-image]: https://badgen.net/github/stars/tgohn/express-session-level?label=%E2%98%85
+
+[![â
][express-session-rsdb-image] express-session-rsdb][express-session-rsdb-url] Session store based on Rocket-Store: A very simple, super fast and yet powerful, flat file database.
+
+[express-session-rsdb-url]: https://www.npmjs.com/package/express-session-rsdb
+[express-session-rsdb-image]: https://badgen.net/github/stars/paragi/express-session-rsdb?label=%E2%98%85
+
+[![â
][express-sessions-image] express-sessions][express-sessions-url] A session store supporting both MongoDB and Redis.
+
+[express-sessions-url]: https://www.npmjs.com/package/express-sessions
+[express-sessions-image]: https://badgen.net/github/stars/konteck/express-sessions?label=%E2%98%85
+
+[![â
][firestore-store-image] firestore-store][firestore-store-url] A [Firestore](https://github.com/hendrysadrak/firestore-store)-based session store.
+
+[firestore-store-url]: https://www.npmjs.com/package/firestore-store
+[firestore-store-image]: https://badgen.net/github/stars/hendrysadrak/firestore-store?label=%E2%98%85
+
+[![â
][fortune-session-image] fortune-session][fortune-session-url] A [Fortune.js](https://github.com/fortunejs/fortune)
+based session store. Supports all backends supported by Fortune (MongoDB, Redis, Postgres, NeDB).
+
+[fortune-session-url]: https://www.npmjs.com/package/fortune-session
+[fortune-session-image]: https://badgen.net/github/stars/aliceklipper/fortune-session?label=%E2%98%85
+
+[![â
][hazelcast-store-image] hazelcast-store][hazelcast-store-url] A Hazelcast-based session store built on the [Hazelcast Node Client](https://www.npmjs.com/package/hazelcast-client).
+
+[hazelcast-store-url]: https://www.npmjs.com/package/hazelcast-store
+[hazelcast-store-image]: https://badgen.net/github/stars/jackspaniel/hazelcast-store?label=%E2%98%85
[![â
][level-session-store-image] level-session-store][level-session-store-url] A LevelDB-based session store.
[level-session-store-url]: https://www.npmjs.com/package/level-session-store
-[level-session-store-image]: https://img.shields.io/github/stars/scriptollc/level-session-store.svg?label=%E2%98%85
+[level-session-store-image]: https://badgen.net/github/stars/toddself/level-session-store?label=%E2%98%85
+
+[![â
][lowdb-session-store-image] lowdb-session-store][lowdb-session-store-url] A [lowdb](https://www.npmjs.com/package/lowdb)-based session store.
+
+[lowdb-session-store-url]: https://www.npmjs.com/package/lowdb-session-store
+[lowdb-session-store-image]: https://badgen.net/github/stars/fhellwig/lowdb-session-store?label=%E2%98%85
+
+[![â
][medea-session-store-image] medea-session-store][medea-session-store-url] A Medea-based session store.
+
+[medea-session-store-url]: https://www.npmjs.com/package/medea-session-store
+[medea-session-store-image]: https://badgen.net/github/stars/BenjaminVadant/medea-session-store?label=%E2%98%85
+
+[![â
][memorystore-image] memorystore][memorystore-url] A memory session store made for production.
+
+[memorystore-url]: https://www.npmjs.com/package/memorystore
+[memorystore-image]: https://badgen.net/github/stars/roccomuso/memorystore?label=%E2%98%85
[![â
][mssql-session-store-image] mssql-session-store][mssql-session-store-url] A SQL Server-based session store.
[mssql-session-store-url]: https://www.npmjs.com/package/mssql-session-store
-[mssql-session-store-image]: https://img.shields.io/github/stars/jwathen/mssql-session-store.svg?label=%E2%98%85
+[mssql-session-store-image]: https://badgen.net/github/stars/jwathen/mssql-session-store?label=%E2%98%85
[![â
][nedb-session-store-image] nedb-session-store][nedb-session-store-url] An alternate NeDB-based (either in-memory or file-persisted) session store.
[nedb-session-store-url]: https://www.npmjs.com/package/nedb-session-store
-[nedb-session-store-image]: https://img.shields.io/github/stars/JamesMGreene/nedb-session-store.svg?label=%E2%98%85
+[nedb-session-store-image]: https://badgen.net/github/stars/JamesMGreene/nedb-session-store?label=%E2%98%85
-[![â
][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](http://sequelizejs.com/).
+[![â
][@quixo3/prisma-session-store-image] @quixo3/prisma-session-store][@quixo3/prisma-session-store-url] A session store for the [Prisma Framework](https://www.prisma.io).
+
+[@quixo3/prisma-session-store-url]: https://www.npmjs.com/package/@quixo3/prisma-session-store
+[@quixo3/prisma-session-store-image]: https://badgen.net/github/stars/kleydon/prisma-session-store?label=%E2%98%85
+
+[![â
][restsession-image] restsession][restsession-url] Store sessions utilizing a RESTful API
+
+[restsession-url]: https://www.npmjs.com/package/restsession
+[restsession-image]: https://badgen.net/github/stars/jankal/restsession?label=%E2%98%85
+
+[![â
][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](https://sequelize.org/).
[sequelstore-connect-url]: https://www.npmjs.com/package/sequelstore-connect
-[sequelstore-connect-image]: https://img.shields.io/github/stars/MattMcFarland/sequelstore-connect.svg?label=%E2%98%85
+[sequelstore-connect-image]: https://badgen.net/github/stars/MattMcFarland/sequelstore-connect?label=%E2%98%85
[![â
][session-file-store-image] session-file-store][session-file-store-url] A file system-based session store.
[session-file-store-url]: https://www.npmjs.com/package/session-file-store
-[session-file-store-image]: https://img.shields.io/github/stars/valery-barysok/session-file-store.svg?label=%E2%98%85
+[session-file-store-image]: https://badgen.net/github/stars/valery-barysok/session-file-store?label=%E2%98%85
+
+[![â
][session-pouchdb-store-image] session-pouchdb-store][session-pouchdb-store-url] Session store for PouchDB / CouchDB. Accepts embedded, custom, or remote PouchDB instance and realtime synchronization.
+
+[session-pouchdb-store-url]: https://www.npmjs.com/package/session-pouchdb-store
+[session-pouchdb-store-image]: https://badgen.net/github/stars/solzimer/session-pouchdb-store?label=%E2%98%85
+
+[![â
][@databunker/session-store-image] @databunker/session-store][@databunker/session-store-url] A [Databunker](https://databunker.org/)-based encrypted session store.
+
+[@databunker/session-store-url]: https://www.npmjs.com/package/@databunker/session-store
+[@databunker/session-store-image]: https://badgen.net/github/stars/securitybunker/databunker-session-store?label=%E2%98%85
+
+[![â
][sessionstore-image] sessionstore][sessionstore-url] A session store that works with various databases.
+
+[sessionstore-url]: https://www.npmjs.com/package/sessionstore
+[sessionstore-image]: https://badgen.net/github/stars/adrai/sessionstore?label=%E2%98%85
-[![â
][session-rethinkdb-image] session-rethinkdb][session-rethinkdb-url] A [RethinkDB](http://rethinkdb.com/)-based session store.
+[![â
][tch-nedb-session-image] tch-nedb-session][tch-nedb-session-url] A file system session store based on NeDB.
-[session-rethinkdb-url]: https://www.npmjs.com/package/session-rethinkdb
-[session-rethinkdb-image]: https://img.shields.io/github/stars/llambda/session-rethinkdb.svg?label=%E2%98%85
+[tch-nedb-session-url]: https://www.npmjs.com/package/tch-nedb-session
+[tch-nedb-session-image]: https://badgen.net/github/stars/tomaschyly/NeDBSession?label=%E2%98%85
-## Example
+## Examples
+
+### View counter
A simple example using `express-session` to store page views for a user.
@@ -615,17 +919,15 @@ app.use(session({
}))
app.use(function (req, res, next) {
- var views = req.session.views
-
- if (!views) {
- views = req.session.views = {}
+ if (!req.session.views) {
+ req.session.views = {}
}
// get the url pathname
var pathname = parseurl(req).pathname
// count the views
- views[pathname] = (views[pathname] || 0) + 1
+ req.session.views[pathname] = (req.session.views[pathname] || 0) + 1
next()
})
@@ -637,19 +939,120 @@ app.get('/foo', function (req, res, next) {
app.get('/bar', function (req, res, next) {
res.send('you viewed this page ' + req.session.views['/bar'] + ' times')
})
+
+app.listen(3000)
+```
+
+### User login
+
+A simple example using `express-session` to keep a user log in session.
+
+```js
+var escapeHtml = require('escape-html')
+var express = require('express')
+var session = require('express-session')
+
+var app = express()
+
+app.use(session({
+ secret: 'keyboard cat',
+ resave: false,
+ saveUninitialized: true
+}))
+
+// middleware to test if authenticated
+function isAuthenticated (req, res, next) {
+ if (req.session.user) next()
+ else next('route')
+}
+
+app.get('/', isAuthenticated, function (req, res) {
+ // this is only called when there is an authentication user due to isAuthenticated
+ res.send('hello, ' + escapeHtml(req.session.user) + '!' +
+ ' Logout')
+})
+
+app.get('/', function (req, res) {
+ res.send('')
+})
+
+app.post('/login', express.urlencoded({ extended: false }), function (req, res) {
+ // login logic to validate req.body.user and req.body.pass
+ // would be implemented here. for this example any combo works
+
+ // regenerate the session, which is good practice to help
+ // guard against forms of session fixation
+ req.session.regenerate(function (err) {
+ if (err) next(err)
+
+ // store user information in session, typically a user id
+ req.session.user = req.body.user
+
+ // save the session before redirection to ensure page
+ // load does not happen before session is saved
+ req.session.save(function (err) {
+ if (err) return next(err)
+ res.redirect('/')
+ })
+ })
+})
+
+app.get('/logout', function (req, res, next) {
+ // logout logic
+
+ // clear the user from the session object and save.
+ // this will ensure that re-using the old session id
+ // does not have a logged in user
+ req.session.user = null
+ req.session.save(function (err) {
+ if (err) next(err)
+
+ // regenerate the session, which is good practice to help
+ // guard against forms of session fixation
+ req.session.regenerate(function (err) {
+ if (err) next(err)
+ res.redirect('/')
+ })
+ })
+})
+
+app.listen(3000)
+```
+
+## Debugging
+
+This module uses the [debug](https://www.npmjs.com/package/debug) module
+internally to log information about session operations.
+
+To see all the internal logs, set the `DEBUG` environment variable to
+`express-session` when launching your app (`npm start`, in this example):
+
+```sh
+$ DEBUG=express-session npm start
+```
+
+On Windows, use the corresponding command;
+
+```sh
+> set DEBUG=express-session & npm start
```
## License
[MIT](LICENSE)
-[npm-image]: https://img.shields.io/npm/v/express-session.svg
-[npm-url]: https://npmjs.org/package/express-session
-[travis-image]: https://img.shields.io/travis/expressjs/session/master.svg
-[travis-url]: https://travis-ci.org/expressjs/session
-[coveralls-image]: https://img.shields.io/coveralls/expressjs/session/master.svg
+[rfc-6265-5.1.4]: https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
+[rfc-6265bis-03-4.1.2.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7
+[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/
+[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1
+[ci-image]: https://badgen.net/github/checks/expressjs/session/master?label=ci
+[ci-url]: https://github.com/expressjs/session/actions?query=workflow%3Aci
+[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/session/master
[coveralls-url]: https://coveralls.io/r/expressjs/session?branch=master
-[downloads-image]: https://img.shields.io/npm/dm/express-session.svg
-[downloads-url]: https://npmjs.org/package/express-session
-[gratipay-image]: https://img.shields.io/gratipay/dougwilson.svg
-[gratipay-url]: https://gratipay.com/dougwilson/
+[node-url]: https://nodejs.org/en/download
+[npm-downloads-image]: https://badgen.net/npm/dm/express-session
+[npm-url]: https://npmjs.org/package/express-session
+[npm-version-image]: https://badgen.net/npm/v/express-session
diff --git a/index.js b/index.js
index 5794118f..3e23e901 100644
--- a/index.js
+++ b/index.js
@@ -13,19 +13,20 @@
* @private
*/
+var Buffer = require('safe-buffer').Buffer
var cookie = require('cookie');
-var crc = require('crc').crc32;
+var crypto = require('crypto')
var debug = require('debug')('express-session');
var deprecate = require('depd')('express-session');
+var onHeaders = require('on-headers')
var parseUrl = require('parseurl');
+var signature = require('cookie-signature')
var uid = require('uid-safe').sync
- , onHeaders = require('on-headers')
- , signature = require('cookie-signature')
+var Cookie = require('./session/cookie')
+var MemoryStore = require('./session/memory')
var Session = require('./session/session')
- , MemoryStore = require('./session/memory')
- , Cookie = require('./session/cookie')
- , Store = require('./session/store')
+var Store = require('./session/store')
// environment
@@ -69,7 +70,7 @@ var defer = typeof setImmediate === 'function'
* Setup session store with the given `options`.
*
* @param {Object} [options]
- * @param {Object} [options.cookie] Options for cookie
+ * @param {Object|Function} [options.cookie] Options for cookie
* @param {Function} [options.genid]
* @param {String} [options.name=connect.sid] Session ID cookie name
* @param {Boolean} [options.proxy]
@@ -148,8 +149,8 @@ function session(options) {
// notify user that this store is not
// meant for a production environment
- if ('production' == env && store instanceof MemoryStore) {
- /* istanbul ignore next: not tested */
+ /* istanbul ignore next: not tested */
+ if (env === 'production' && store instanceof MemoryStore) {
console.warn(warning);
}
@@ -157,10 +158,17 @@ function session(options) {
store.generate = function(req){
req.sessionID = generateId(req);
req.session = new Session(req);
- req.session.cookie = new Cookie(cookieOptions);
+ var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions;
+ req.session.cookie = new Cookie(resolvedCookieOptions);
- if (cookieOptions.secure === 'auto') {
- req.session.cookie.secure = issecure(req, trustProxy);
+ var isSecure = issecure(req, trustProxy);
+
+ if (resolvedCookieOptions.secure === 'auto') {
+ req.session.cookie.secure = isSecure;
+ }
+
+ if (resolvedCookieOptions.sameSite === 'auto') {
+ req.session.cookie.sameSite = isSecure ? 'none' : 'lax';
}
};
@@ -191,8 +199,16 @@ function session(options) {
}
// pathname mismatch
- var originalPath = parseUrl.original(req).pathname;
- if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next();
+ var originalPath = parseUrl.original(req).pathname || '/'
+ var resolvedCookieOptions = typeof cookieOptions === 'function' ? cookieOptions(req) : cookieOptions
+ var cfgPath = resolvedCookieOptions.path || '/'
+
+ if (!rfcPathMatch(originalPath, cfgPath)) {
+ debug('pathname mismatch')
+ next()
+ return
+ }
+
// ensure a secret is available or bail
if (!secret && !req.secret) {
@@ -207,6 +223,7 @@ function session(options) {
var originalHash;
var originalId;
var savedHash;
+ var touched = false
// expose store
req.sessionStore = store;
@@ -231,11 +248,18 @@ function session(options) {
return;
}
- // touch session
- req.session.touch();
+ if (!touched) {
+ // touch session
+ req.session.touch()
+ touched = true
+ }
// set cookie
- setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
+ try {
+ setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data)
+ } catch (err) {
+ defer(next, err)
+ }
});
// proxy end() to commit the session
@@ -267,6 +291,10 @@ function session(options) {
return ret;
}
+ if (!res._header) {
+ res._implicitHeader()
+ }
+
if (chunk == null) {
ret = true;
return ret;
@@ -277,7 +305,7 @@ function session(options) {
if (!isNaN(contentLength) && contentLength > 0) {
// measure chunk
chunk = !Buffer.isBuffer(chunk)
- ? new Buffer(chunk, encoding)
+ ? Buffer.from(chunk, encoding)
: chunk;
encoding = undefined;
@@ -316,6 +344,12 @@ function session(options) {
return _end.call(res, chunk, encoding);
}
+ if (!touched) {
+ // touch session
+ req.session.touch()
+ touched = true
+ }
+
if (shouldSave(req)) {
req.session.save(function onsave(err) {
if (err) {
@@ -352,16 +386,52 @@ function session(options) {
wrapmethods(req.session);
}
+ // inflate the session
+ function inflate (req, sess) {
+ store.createSession(req, sess)
+ originalId = req.sessionID
+ originalHash = hash(sess)
+
+ if (!resaveSession) {
+ savedHash = originalHash
+ }
+
+ wrapmethods(req.session)
+ }
+
+ function rewrapmethods (sess, callback) {
+ return function () {
+ if (req.session !== sess) {
+ wrapmethods(req.session)
+ }
+
+ callback.apply(this, arguments)
+ }
+ }
+
// wrap session methods
function wrapmethods(sess) {
+ var _reload = sess.reload
var _save = sess.save;
+ function reload(callback) {
+ debug('reloading %s', this.id)
+ _reload.call(this, rewrapmethods(this, callback))
+ }
+
function save() {
debug('saving %s', this.id);
savedHash = hash(this);
_save.apply(this, arguments);
}
+ Object.defineProperty(sess, 'reload', {
+ configurable: true,
+ enumerable: false,
+ value: reload,
+ writable: true
+ })
+
Object.defineProperty(sess, 'save', {
configurable: true,
enumerable: false,
@@ -393,7 +463,7 @@ function session(options) {
return false;
}
- return !saveUninitializedSession && cookieId !== req.sessionID
+ return !saveUninitializedSession && !savedHash && cookieId !== req.sessionID
? isModified(req.session)
: !isSaved(req.session)
}
@@ -416,7 +486,7 @@ function session(options) {
return false;
}
- return cookieId != req.sessionID
+ return cookieId !== req.sessionID
? saveUninitializedSession || isModified(req.session)
: rollingSessions || req.session.cookie.expires != null && isModified(req.session);
}
@@ -433,38 +503,66 @@ function session(options) {
debug('fetching %s', req.sessionID);
store.get(req.sessionID, function(err, sess){
// error handling
- if (err) {
+ if (err && err.code !== 'ENOENT') {
debug('error %j', err);
+ next(err)
+ return
+ }
- if (err.code !== 'ENOENT') {
- next(err);
- return;
- }
-
- generate();
- // no session
- } else if (!sess) {
- debug('no session found');
- generate();
- // populate req.session
- } else {
- debug('session found');
- store.createSession(req, sess);
- originalId = req.sessionID;
- originalHash = hash(sess);
-
- if (!resaveSession) {
- savedHash = originalHash
+ try {
+ if (err || !sess) {
+ debug('no session found')
+ generate()
+ } else {
+ debug('session found')
+ inflate(req, sess)
}
-
- wrapmethods(req.session);
+ } catch (e) {
+ next(e)
+ return
}
- next();
+ next()
});
};
};
+/**
+ * Check if the cookiePath matches the requestPath following the
+ * rules in RFC 6265 section 5.1.4.
+ *
+ * @param {String} requestPath
+ * @param {String} cookiePath
+ * @return {Boolean}
+ * @private
+ */
+
+function rfcPathMatch(requestPath, cookiePath) {
+ // Normalize inputs (Node 0.8-safe)
+ requestPath = (typeof requestPath === 'string' && requestPath.length) ? requestPath : '/';
+ cookiePath = (typeof cookiePath === 'string' && cookiePath.length) ? cookiePath : '/';
+
+ // Root cookie matches everything
+ if (cookiePath === '/') return true;
+
+ // Exact match
+ if (requestPath === cookiePath) return true;
+
+ // Prefix match
+ if (requestPath.indexOf(cookiePath) === 0) {
+ // If cookiePath ends with '/', any longer requestPath is OK
+ if (cookiePath.charAt(cookiePath.length - 1) === '/') return true;
+
+ // Otherwise the next char after the prefix must be '/'
+ var nextChar = requestPath.length > cookiePath.length
+ ? requestPath.charAt(cookiePath.length)
+ : '';
+ return nextChar === '/';
+ }
+
+ return false;
+}
+
/**
* Generate a session ID for a new session.
*
@@ -472,7 +570,7 @@ function session(options) {
* @private
*/
-function generateSessionId(sess) {
+function generateSessionId() {
return uid(24);
}
@@ -495,7 +593,7 @@ function getcookie(req, name, secrets) {
raw = cookies[name];
if (raw) {
- if (raw.substr(0, 2) === 's:') {
+ if (raw.slice(0, 2) === 's:') {
val = unsigncookie(raw.slice(2), secrets);
if (val === false) {
@@ -522,7 +620,7 @@ function getcookie(req, name, secrets) {
raw = req.cookies[name];
if (raw) {
- if (raw.substr(0, 2) === 's:') {
+ if (raw.slice(0, 2) === 's:') {
val = unsigncookie(raw.slice(2), secrets);
if (val) {
@@ -551,11 +649,21 @@ function getcookie(req, name, secrets) {
*/
function hash(sess) {
- return crc(JSON.stringify(sess, function (key, val) {
- if (key !== 'cookie') {
- return val;
+ // serialize
+ var str = JSON.stringify(sess, function (key, val) {
+ // ignore sess.cookie property
+ if (this === sess && key === 'cookie') {
+ return
}
- }));
+
+ return val
+ })
+
+ // hash
+ return crypto
+ .createHash('sha1')
+ .update(str, 'utf8')
+ .digest('hex')
}
/**
@@ -569,7 +677,8 @@ function hash(sess) {
function issecure(req, trustProxy) {
// socket is https server
- if (req.connection && req.connection.encrypted) {
+ var socket = req.socket || req.connection;
+ if (socket && socket.encrypted) {
return true;
}
@@ -580,17 +689,14 @@ function issecure(req, trustProxy) {
// no explicit trust; try req.secure from express
if (trustProxy !== true) {
- var secure = req.secure;
- return typeof secure === 'boolean'
- ? secure
- : false;
+ return req.secure === true
}
// read the proto from x-forwarded-proto header
var header = req.headers['x-forwarded-proto'] || '';
var index = header.indexOf(',');
var proto = index !== -1
- ? header.substr(0, index).toLowerCase().trim()
+ ? header.slice(0, index).toLowerCase().trim()
: header.toLowerCase().trim()
return proto === 'https';
@@ -608,12 +714,10 @@ function setcookie(res, name, val, secret, options) {
debug('set-cookie %s', data);
- var prev = res.getHeader('set-cookie') || [];
- var header = Array.isArray(prev) ? prev.concat(data)
- : Array.isArray(data) ? [prev].concat(data)
- : [prev, data];
+ var prev = res.getHeader('Set-Cookie') || []
+ var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];
- res.setHeader('set-cookie', header)
+ res.setHeader('Set-Cookie', header)
}
/**
diff --git a/package.json b/package.json
index d1852719..36387796 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "express-session",
- "version": "1.14.0",
+ "version": "1.19.0",
"description": "Simple session middleware for Express",
"author": "TJ Holowaychuk (http://tjholowaychuk.com)",
"contributors": [
@@ -8,38 +8,43 @@
"Joe Wagner "
],
"repository": "expressjs/session",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ },
"license": "MIT",
"dependencies": {
- "cookie": "0.3.1",
- "cookie-signature": "1.0.6",
- "crc": "3.4.0",
- "debug": "~2.2.0",
- "depd": "~1.1.0",
- "on-headers": "~1.0.1",
- "parseurl": "~1.3.1",
- "uid-safe": "~2.1.1",
- "utils-merge": "1.0.0"
+ "cookie": "~0.7.2",
+ "cookie-signature": "~1.0.7",
+ "debug": "~2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.1.0",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "~5.2.1",
+ "uid-safe": "~2.1.5"
},
"devDependencies": {
- "after": "0.8.1",
- "cookie-parser": "1.4.3",
- "express": "4.14.0",
- "istanbul": "0.4.4",
- "mocha": "2.5.3",
- "supertest": "1.1.0"
+ "after": "0.8.2",
+ "cookie-parser": "1.4.6",
+ "eslint": "8.56.0",
+ "eslint-plugin-markdown": "3.0.1",
+ "express": "4.17.3",
+ "mocha": "10.8.2",
+ "nyc": "15.1.0",
+ "supertest": "6.3.4"
},
"files": [
"session/",
- "HISTORY.md",
- "LICENSE",
"index.js"
],
"engines": {
"node": ">= 0.8.0"
},
"scripts": {
- "test": "mocha --bail --reporter spec test/",
- "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot test/",
- "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec test/"
+ "lint": "eslint . && node ./scripts/lint-readme.js",
+ "test": "./test/support/gencert.sh && mocha --require test/support/env --check-leaks --no-exit --reporter spec test/",
+ "test-ci": "nyc --reporter=lcov --reporter=text npm test",
+ "test-cov": "nyc npm test",
+ "version": "node scripts/version-history.js && git add HISTORY.md"
}
}
diff --git a/scripts/lint-readme.js b/scripts/lint-readme.js
new file mode 100644
index 00000000..f4c50d76
--- /dev/null
+++ b/scripts/lint-readme.js
@@ -0,0 +1,93 @@
+var assert = require('assert')
+var fs = require('fs')
+var path = require('path')
+
+var BADGE_STAR_LINK_REGEXP = /^https:\/\/badgen.net\/github\/stars\/[^/]+\/[^/]+?label=%E2%98%85$/
+var MARKDOWN_LINK_REGEXP = /^\[([^ \]]+)\]: ([^ ]+)$/
+var MARKDOWN_SECTION_REGEXP = /^#+ (.+)$/
+var NEWLINE_REGEXP = /\r?\n/
+var README_PATH = path.join(__dirname, '..', 'README.md')
+var README_CONTENTS = fs.readFileSync(README_PATH, 'utf-8')
+var STORE_HEADER_REGEXP = /^\[!\[â
\]\[([^ \]]+)\] ([^ \]]+)\]\[([^ \]]+)\](?: .+)?$/
+
+var header = null
+var lintedStores = false
+var section = null
+var state = 0
+
+README_CONTENTS.split(NEWLINE_REGEXP).forEach(function (line, lineidx) {
+ section = (MARKDOWN_SECTION_REGEXP.exec(line) || [])[1] || section
+
+ if (section === 'Compatible Session Stores') {
+ lintedStores = true
+
+ switch (state) {
+ case 0: // premble
+ if (line[0] !== '[') break
+ state = 1
+ case 1: // header
+ var prev = header
+
+ if (!(header = STORE_HEADER_REGEXP.exec(line))) {
+ expect(lineidx, 'session store header', line)
+ } else if (prev && prev[2].replace(/^[^/]+\//, '').localeCompare(header[2].replace(/^[^/]+\//, '')) > 0) {
+ expect(lineidx, (header[2] + ' to be alphabetically before ' + prev[2]), line)
+ state = 2
+ } else {
+ state = 2
+ }
+
+ break
+ case 2: // blank line
+ if (line && MARKDOWN_LINK_REGEXP.test(line)) {
+ expect(lineidx, 'blank line or wrapped description', line)
+ } else if (line === '') {
+ state = 3
+ }
+ break
+ case 3: // url link
+ var urlLink = MARKDOWN_LINK_REGEXP.exec(line)
+
+ if (!urlLink) {
+ expect(lineidx, 'link reference', line)
+ } else if (urlLink[1] !== header[3]) {
+ expect(lineidx, ('link name of ' + header[3]), line)
+ } else if (urlLink[2] !== ('https://www.npmjs.com/package/' + header[2])) {
+ expect(lineidx, ('link url of https://www.npmjs.com/package/' + header[2]), line)
+ } else {
+ state = 4
+ }
+
+ break
+ case 4: // image link
+ var imageLink = MARKDOWN_LINK_REGEXP.exec(line)
+
+ if (!imageLink) {
+ expect(lineidx, 'link reference', line)
+ } else if (imageLink[1] !== header[1]) {
+ expect(lineidx, ('link name of ' + header[1]), line)
+ } else if (!BADGE_STAR_LINK_REGEXP.test(imageLink[2])) {
+ expect(lineidx, ('link url to github stars badge'), line)
+ } else {
+ state = 5
+ }
+
+ break
+ case 5: // blank line
+ if (line !== '') {
+ expect(lineidx, 'blank line after links', line)
+ } else {
+ state = 1
+ }
+ break
+ }
+ }
+})
+
+assert.ok(lintedStores, 'Compatible Session Stores section linted')
+
+function expect (lineidx, message, line) {
+ console.log('Expected %s on line %d', message, (lineidx + 1))
+ console.log(' Got: %s', line)
+ process.exitCode = 1
+}
diff --git a/scripts/version-history.js b/scripts/version-history.js
new file mode 100644
index 00000000..c58268ad
--- /dev/null
+++ b/scripts/version-history.js
@@ -0,0 +1,63 @@
+'use strict'
+
+var fs = require('fs')
+var path = require('path')
+
+var HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md')
+var MD_HEADER_REGEXP = /^====*$/
+var VERSION = process.env.npm_package_version
+var VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/
+
+var historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n')
+
+if (!MD_HEADER_REGEXP.test(historyFileLines[1])) {
+ console.error('Missing header in HISTORY.md')
+ process.exit(1)
+}
+
+if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) {
+ console.error('Missing placeholder version in HISTORY.md')
+ process.exit(1)
+}
+
+if (historyFileLines[0].indexOf('x') !== -1) {
+ var versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$')
+
+ if (!versionCheckRegExp.test(VERSION)) {
+ console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0])
+ process.exit(1)
+ }
+}
+
+historyFileLines[0] = VERSION + ' / ' + getLocaleDate()
+historyFileLines[1] = repeat('=', historyFileLines[0].length)
+
+fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n'))
+
+function getLocaleDate () {
+ var now = new Date()
+
+ return zeroPad(now.getFullYear(), 4) + '-' +
+ zeroPad(now.getMonth() + 1, 2) + '-' +
+ zeroPad(now.getDate(), 2)
+}
+
+function repeat (str, length) {
+ var out = ''
+
+ for (var i = 0; i < length; i++) {
+ out += str
+ }
+
+ return out
+}
+
+function zeroPad (number, length) {
+ var num = number.toString()
+
+ while (num.length < length) {
+ num = '0' + num
+ }
+
+ return num
+}
diff --git a/session/cookie.js b/session/cookie.js
index a4dbd8a8..8bb5907b 100644
--- a/session/cookie.js
+++ b/session/cookie.js
@@ -11,8 +11,8 @@
* Module dependencies.
*/
-var merge = require('utils-merge')
- , cookie = require('cookie');
+var cookie = require('cookie')
+var deprecate = require('depd')('express-session')
/**
* Initialize a new `Cookie` with the given `options`.
@@ -26,10 +26,22 @@ var Cookie = module.exports = function Cookie(options) {
this.path = '/';
this.maxAge = null;
this.httpOnly = true;
- if (options) merge(this, options);
- this.originalMaxAge = undefined == this.originalMaxAge
- ? this.maxAge
- : this.originalMaxAge;
+
+ if (options) {
+ if (typeof options !== 'object') {
+ throw new TypeError('argument options must be a object')
+ }
+
+ for (var key in options) {
+ if (key !== 'data') {
+ this[key] = options[key]
+ }
+ }
+ }
+
+ if (this.originalMaxAge === undefined || this.originalMaxAge === null) {
+ this.originalMaxAge = this.maxAge
+ }
};
/*!
@@ -69,7 +81,15 @@ Cookie.prototype = {
*/
set maxAge(ms) {
- this.expires = 'number' == typeof ms
+ if (ms && typeof ms !== 'number' && !(ms instanceof Date)) {
+ throw new TypeError('maxAge must be a number or Date')
+ }
+
+ if (ms instanceof Date) {
+ deprecate('maxAge as Date; pass number of milliseconds instead')
+ }
+
+ this.expires = typeof ms === 'number'
? new Date(Date.now() + ms)
: ms;
},
@@ -96,12 +116,15 @@ Cookie.prototype = {
get data() {
return {
- originalMaxAge: this.originalMaxAge
+ originalMaxAge: this.originalMaxAge,
+ partitioned: this.partitioned,
+ priority: this.priority
, expires: this._expires
, secure: this.secure
, httpOnly: this.httpOnly
, domain: this.domain
, path: this.path
+ , sameSite: this.sameSite
}
},
diff --git a/session/memory.js b/session/memory.js
index 25252b6c..11ed686c 100644
--- a/session/memory.js
+++ b/session/memory.js
@@ -171,14 +171,16 @@ function getSession(sessionId) {
// parse
sess = JSON.parse(sess)
- var expires = typeof sess.cookie.expires === 'string'
- ? new Date(sess.cookie.expires)
- : sess.cookie.expires
-
- // destroy expired session
- if (expires && expires <= Date.now()) {
- delete this.sessions[sessionId]
- return
+ if (sess.cookie) {
+ var expires = typeof sess.cookie.expires === 'string'
+ ? new Date(sess.cookie.expires)
+ : sess.cookie.expires
+
+ // destroy expired session
+ if (expires && expires <= Date.now()) {
+ delete this.sessions[sessionId]
+ return
+ }
}
return sess
diff --git a/session/session.js b/session/session.js
index 2eacde60..fee7608c 100644
--- a/session/session.js
+++ b/session/session.js
@@ -87,7 +87,8 @@ defineMethod(Session.prototype, 'save', function save(fn) {
defineMethod(Session.prototype, 'reload', function reload(fn) {
var req = this.req
- , store = this.req.sessionStore;
+ var store = this.req.sessionStore
+
store.get(this.id, function(err, sess){
if (err) return fn(err);
if (!sess) return fn(new Error('failed to load session'));
diff --git a/session/store.js b/session/store.js
index 387469c5..3793877e 100644
--- a/session/store.js
+++ b/session/store.js
@@ -85,10 +85,18 @@ Store.prototype.load = function(sid, fn){
Store.prototype.createSession = function(req, sess){
var expires = sess.cookie.expires
- , orig = sess.cookie.originalMaxAge;
+ var originalMaxAge = sess.cookie.originalMaxAge
+
sess.cookie = new Cookie(sess.cookie);
- if ('string' == typeof expires) sess.cookie.expires = new Date(expires);
- sess.cookie.originalMaxAge = orig;
+
+ if (typeof expires === 'string') {
+ // convert expires to a Date object
+ sess.cookie.expires = new Date(expires)
+ }
+
+ // keep originalMaxAge intact
+ sess.cookie.originalMaxAge = originalMaxAge
+
req.session = new Session(req, sess);
return req.session;
};
diff --git a/test/cookie.js b/test/cookie.js
new file mode 100644
index 00000000..ea676e35
--- /dev/null
+++ b/test/cookie.js
@@ -0,0 +1,134 @@
+
+var assert = require('assert')
+var Cookie = require('../session/cookie')
+
+describe('new Cookie()', function () {
+ it('should create a new cookie object', function () {
+ assert.strictEqual(typeof new Cookie(), 'object')
+ })
+
+ it('should default expires to null', function () {
+ var cookie = new Cookie()
+ assert.strictEqual(cookie.expires, null)
+ })
+
+ it('should default httpOnly to true', function () {
+ var cookie = new Cookie()
+ assert.strictEqual(cookie.httpOnly, true)
+ })
+
+ it('should default path to "/"', function () {
+ var cookie = new Cookie()
+ assert.strictEqual(cookie.path, '/')
+ })
+
+ it('should default maxAge to null', function () {
+ var cookie = new Cookie()
+ assert.strictEqual(cookie.maxAge, null)
+ })
+
+ describe('with options', function () {
+ it('should create a new cookie object', function () {
+ assert.strictEqual(typeof new Cookie({}), 'object')
+ })
+
+ it('should reject non-objects', function () {
+ assert.throws(function () { new Cookie(42) }, /argument options/)
+ assert.throws(function () { new Cookie('foo') }, /argument options/)
+ assert.throws(function () { new Cookie(true) }, /argument options/)
+ assert.throws(function () { new Cookie(function () {}) }, /argument options/)
+ })
+
+ it('should ignore "data" option', function () {
+ var cookie = new Cookie({ data: { foo: 'bar' }, path: '/foo' })
+
+ assert.strictEqual(typeof cookie, 'object')
+ assert.strictEqual(typeof cookie.data, 'object')
+ assert.strictEqual(cookie.data.path, '/foo')
+ assert.notStrictEqual(cookie.data.foo, 'bar')
+ })
+
+ describe('expires', function () {
+ it('should set expires', function () {
+ var expires = new Date(Date.now() + 60000)
+ var cookie = new Cookie({ expires: expires })
+
+ assert.strictEqual(cookie.expires, expires)
+ })
+
+ it('should set maxAge', function () {
+ var expires = new Date(Date.now() + 60000)
+ var cookie = new Cookie({ expires: expires })
+
+ assert.ok(expires.getTime() - Date.now() - 1000 <= cookie.maxAge)
+ assert.ok(expires.getTime() - Date.now() + 1000 >= cookie.maxAge)
+ })
+ })
+
+ describe('httpOnly', function () {
+ it('should set httpOnly', function () {
+ var cookie = new Cookie({ httpOnly: false })
+
+ assert.strictEqual(cookie.httpOnly, false)
+ })
+ })
+
+ describe('maxAge', function () {
+ it('should set expires', function () {
+ var maxAge = 60000
+ var cookie = new Cookie({ maxAge: maxAge })
+
+ assert.ok(cookie.expires.getTime() - Date.now() - 1000 <= maxAge)
+ assert.ok(cookie.expires.getTime() - Date.now() + 1000 >= maxAge)
+ })
+
+ it('should set maxAge', function () {
+ var maxAge = 60000
+ var cookie = new Cookie({ maxAge: maxAge })
+
+ assert.strictEqual(typeof cookie.maxAge, 'number')
+ assert.ok(cookie.maxAge - 1000 <= maxAge)
+ assert.ok(cookie.maxAge + 1000 >= maxAge)
+ })
+
+ it('should accept Date object', function () {
+ var maxAge = new Date(Date.now() + 60000)
+ var cookie = new Cookie({ maxAge: maxAge })
+
+ assert.strictEqual(cookie.expires.getTime(), maxAge.getTime())
+ assert.ok(maxAge.getTime() - Date.now() - 1000 <= cookie.maxAge)
+ assert.ok(maxAge.getTime() - Date.now() + 1000 >= cookie.maxAge)
+ })
+
+ it('should reject invalid types', function() {
+ assert.throws(function() { new Cookie({ maxAge: '42' }) }, /maxAge/)
+ assert.throws(function() { new Cookie({ maxAge: true }) }, /maxAge/)
+ assert.throws(function() { new Cookie({ maxAge: function () {} }) }, /maxAge/)
+ })
+ })
+
+ describe('partitioned', function () {
+ it('should set partitioned', function () {
+ var cookie = new Cookie({ partitioned: true })
+
+ assert.strictEqual(cookie.partitioned, true)
+ })
+ })
+
+ describe('path', function () {
+ it('should set path', function () {
+ var cookie = new Cookie({ path: '/foo' })
+
+ assert.strictEqual(cookie.path, '/foo')
+ })
+ })
+
+ describe('priority', function () {
+ it('should set priority', function () {
+ var cookie = new Cookie({ priority: 'high' })
+
+ assert.strictEqual(cookie.priority, 'high')
+ })
+ })
+ })
+})
diff --git a/test/fixtures/.gitkeep b/test/fixtures/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixtures/server.crt b/test/fixtures/server.crt
deleted file mode 100644
index c019198a..00000000
--- a/test/fixtures/server.crt
+++ /dev/null
@@ -1,14 +0,0 @@
------BEGIN CERTIFICATE-----
-MIICMzCCAZwCCQCJTms0qcIZgDANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQGEwJV
-UzEQMA4GA1UECBMHSW5kaWFuYTEdMBsGA1UEChMUbm9kZS1leHByZXNzLXNlc3Np
-b24xHjAcBgNVBAMTFWV4cHJlc3Mtc2Vzc2lvbi5sb2NhbDAeFw0xNDExMjMwNTQ3
-MzlaFw0yNDExMjAwNTQ3MzlaMF4xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdJbmRp
-YW5hMR0wGwYDVQQKExRub2RlLWV4cHJlc3Mtc2Vzc2lvbjEeMBwGA1UEAxMVZXhw
-cmVzcy1zZXNzaW9uLmxvY2FsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDY
-G398zqN6Yv/FAx77eoLLje4VNCYDXUciWBzceaZ5u/rtP/XshTQWDKFfUbb/kmni
-DazsCasSoNsUCrcqQPJF1jF3F9XjsZxcggDab8MAbAMhnhJax2l3yPJsWnB/8mrY
-cdsdQNPXJxQW9MMZgUxz73gIY5/rEeayU9owgcnVcQIDAQABMA0GCSqGSIb3DQEB
-BQUAA4GBADi0XXsZlQAKxOyD4qvdJNWJXlfqhr1q53dZmfF7nikDaMDiMspczTS/
-pxNbq2UaMc7g+6qmXPaPQoN3laQQtMdyVfSh6EIJHbzXXzdcCT4fDBYX7iwvh0Gg
-DUpjmxmCzFCaob9+hZzwbJi5MFQ4Qq12LW1aYHMs8wgLboHml1WL
------END CERTIFICATE-----
diff --git a/test/fixtures/server.key b/test/fixtures/server.key
deleted file mode 100644
index dabf2b83..00000000
--- a/test/fixtures/server.key
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXQIBAAKBgQDYG398zqN6Yv/FAx77eoLLje4VNCYDXUciWBzceaZ5u/rtP/Xs
-hTQWDKFfUbb/kmniDazsCasSoNsUCrcqQPJF1jF3F9XjsZxcggDab8MAbAMhnhJa
-x2l3yPJsWnB/8mrYcdsdQNPXJxQW9MMZgUxz73gIY5/rEeayU9owgcnVcQIDAQAB
-AoGBALB8HGw/kPA1Ay3Qc6/qCADWYvW8BcM/nQUmMkO3sUW/R5gTYPIMglHzdKIU
-aL9kwcXTZ0HIT4ZCCUffzF/cdD0lqCZjwGl0aM4xUZcPbaM/KmZOUcP92ymsN+rF
-uuJXks6hxrmJ5hh5D6FXdlQjTCdG3u9w0+KD4n1BkBkEaqflAkEA9q4TBt1bLOvV
-bbwz7bunI6bwS5eD7sVn1qUqrm6hcirhF1xCv75pO/uqAyccbguxjp8y2LNuN+cO
-IgSwr+Jq1wJBAOBFuKSMAcg2WmJ4IAT7143yhoHKMgA8Nl4sfLFwFl2/hw+fdEBe
-gndRfKHT4IV/YLcnS7d/H6mEMSAusWx5QPcCQQDTYaF+TWrW2JRAf3jEK/xyiZf6
-PrDYh6KOhWRIqxZ/fYz69p1gL6t/sg0ivH4ZMr4JKBRrK360OrOapQg+/7drAkB6
-3pfPRok/aE/SfN+F+3fX49Q/TUhhipt6ssLJ74/BYsobDBADqAOwXSt7+XmbifKx
-xUydRn9RPwQvDoXT2QZ3AkAdnwR9PMEHAsaibrPyzBztKjPL2rWBRk1QAmcdrTpt
-XL99XfmkERWtiBA/Lz2K332qYOa/zo5c/SADx9fm7+HP
------END RSA PRIVATE KEY-----
diff --git a/test/session.js b/test/session.js
index f9419d48..405e3ddf 100644
--- a/test/session.js
+++ b/test/session.js
@@ -1,37 +1,38 @@
-process.env.NO_DEPRECATION = 'express-session';
-
var after = require('after')
var assert = require('assert')
+var cookieParser = require('cookie-parser')
+var crypto = require('crypto')
var express = require('express')
- , request = require('supertest')
- , cookieParser = require('cookie-parser')
- , session = require('../')
- , Cookie = require('../session/cookie')
var fs = require('fs')
var http = require('http')
var https = require('https')
-var util = require('util')
+var request = require('supertest')
+var session = require('../')
+var SmartStore = require('./support/smart-store')
+var SyncStore = require('./support/sync-store')
+var utils = require('./support/utils')
+
+var Cookie = require('../session/cookie')
var min = 60 * 1000;
describe('session()', function(){
it('should export constructors', function(){
- assert.equal(typeof session.Session, 'function')
- assert.equal(typeof session.Store, 'function')
- assert.equal(typeof session.MemoryStore, 'function')
+ assert.strictEqual(typeof session.Session, 'function')
+ assert.strictEqual(typeof session.Store, 'function')
+ assert.strictEqual(typeof session.MemoryStore, 'function')
})
it('should do nothing if req.session exists', function(done){
- var app = express()
- .use(function(req, res, next){ req.session = {}; next(); })
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
- .use(end);
+ function setup (req) {
+ req.session = {}
+ }
- request(app)
- .get('/')
- .expect(shouldNotHaveHeader('Set-Cookie'))
- .expect(200, done)
+ request(createServer(setup))
+ .get('/')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
})
it('should error without secret', function(done){
@@ -41,13 +42,11 @@ describe('session()', function(){
})
it('should get secret from req.secret', function(done){
- var app = express()
- .use(function(req, res, next){ req.secret = 'keyboard cat'; next(); })
- .use(session({ cookie: { maxAge: min }}))
- .use(end);
- app.set('env', 'test');
+ function setup (req) {
+ req.secret = 'keyboard cat'
+ }
- request(app)
+ request(createServer(setup, { secret: undefined }))
.get('/')
.expect(200, '', done)
})
@@ -66,7 +65,7 @@ describe('session()', function(){
if (err) return done(err)
store.length(function (err, len) {
if (err) return done(err)
- assert.equal(len, 1)
+ assert.strictEqual(len, 1)
done()
})
})
@@ -153,7 +152,7 @@ describe('session()', function(){
if (err) return done(err)
store.all(function (err, sess) {
if (err) return done(err)
- assert.equal(Object.keys(sess).length, 2)
+ assert.strictEqual(Object.keys(sess).length, 2)
done()
})
}
@@ -167,17 +166,25 @@ describe('session()', function(){
.expect(200, 'session created', cb)
})
+ it('should handle empty req.url', function (done) {
+ function setup (req) {
+ req.url = ''
+ }
+
+ request(createServer(setup))
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
it('should handle multiple res.end calls', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
- .use(function(req, res){
- res.setHeader('Content-Type', 'text/plain');
- res.end('Hello, world!');
- res.end();
- });
- app.set('env', 'test');
+ var server = createServer(null, function (req, res) {
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('Hello, world!')
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.expect('Content-Type', 'text/plain')
.expect(200, 'Hello, world!', done);
@@ -250,35 +257,33 @@ describe('session()', function(){
if (err) return done(err)
store.length(function (err, length) {
if (err) return done(err)
- assert.equal(length, 0)
+ assert.strictEqual(length, 0)
done()
})
})
})
it('should update cookie expiration when slow write', function (done) {
- var app = express();
- app.use(session({ rolling: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function (req, res, next) {
- req.session.user = 'bob';
- res.write('hello, ');
+ var server = createServer({ rolling: true }, function (req, res) {
+ req.session.user = 'bob'
+ res.write('hello, ')
setTimeout(function () {
- res.end('world!');
- }, 200);
- });
+ res.end('world!')
+ }, 200)
+ })
- request(app)
+ request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function (err, res) {
if (err) return done(err);
var originalExpires = expires(res);
setTimeout(function () {
- request(app)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(shouldSetCookie('connect.sid'))
- .expect(function (res) { assert.notEqual(originalExpires, expires(res)); })
+ .expect(function (res) { assert.notStrictEqual(originalExpires, expires(res)); })
.expect(200, done);
}, (1000 - (Date.now() % 1000) + 200));
});
@@ -286,34 +291,21 @@ describe('session()', function(){
describe('when response ended', function () {
it('should have saved session', function (done) {
- var saved = false
var store = new session.MemoryStore()
var server = createServer({ store: store }, function (req, res) {
req.session.hit = true
res.end('session saved')
})
- var _set = store.set
- store.set = function set(sid, sess, callback) {
- setTimeout(function () {
- _set.call(store, sid, sess, function (err) {
- saved = true
- callback(err)
- })
- }, 200)
- }
-
request(server)
- .get('/')
- .expect(200, 'session saved', function (err) {
- if (err) return done(err)
- assert.ok(saved)
- done()
- })
+ .get('/')
+ .expect(200)
+ .expect(shouldSetSessionInStore(store, 200))
+ .expect('session saved')
+ .end(done)
})
it('should have saved session even with empty response', function (done) {
- var saved = false
var store = new session.MemoryStore()
var server = createServer({ store: store }, function (req, res) {
req.session.hit = true
@@ -321,27 +313,14 @@ describe('session()', function(){
res.end()
})
- var _set = store.set
- store.set = function set(sid, sess, callback) {
- setTimeout(function () {
- _set.call(store, sid, sess, function (err) {
- saved = true
- callback(err)
- })
- }, 200)
- }
-
request(server)
- .get('/')
- .expect(200, '', function (err) {
- if (err) return done(err)
- assert.ok(saved)
- done()
- })
+ .get('/')
+ .expect(200)
+ .expect(shouldSetSessionInStore(store, 200))
+ .end(done)
})
it('should have saved session even with multi-write', function (done) {
- var saved = false
var store = new session.MemoryStore()
var server = createServer({ store: store }, function (req, res) {
req.session.hit = true
@@ -350,27 +329,15 @@ describe('session()', function(){
res.end('world')
})
- var _set = store.set
- store.set = function set(sid, sess, callback) {
- setTimeout(function () {
- _set.call(store, sid, sess, function (err) {
- saved = true
- callback(err)
- })
- }, 200)
- }
-
request(server)
- .get('/')
- .expect(200, 'hello, world', function (err) {
- if (err) return done(err)
- assert.ok(saved)
- done()
- })
+ .get('/')
+ .expect(200)
+ .expect(shouldSetSessionInStore(store, 200))
+ .expect('hello, world')
+ .end(done)
})
it('should have saved session even with non-chunked response', function (done) {
- var saved = false
var store = new session.MemoryStore()
var server = createServer({ store: store }, function (req, res) {
req.session.hit = true
@@ -378,22 +345,48 @@ describe('session()', function(){
res.end('session saved')
})
- var _set = store.set
- store.set = function set(sid, sess, callback) {
- setTimeout(function () {
- _set.call(store, sid, sess, function (err) {
- saved = true
- callback(err)
- })
- }, 200)
- }
+ request(server)
+ .get('/')
+ .expect(200)
+ .expect(shouldSetSessionInStore(store, 200))
+ .expect('session saved')
+ .end(done)
+ })
+
+ it('should have saved session with updated cookie expiration', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ cookie: { maxAge: min }, store: store }, function (req, res) {
+ req.session.user = 'bob'
+ res.end(req.session.id)
+ })
request(server)
.get('/')
- .expect(200, 'session saved', function (err) {
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, function (err, res) {
if (err) return done(err)
- assert.ok(saved)
- done()
+ var id = res.text
+ store.get(id, function (err, sess) {
+ if (err) return done(err)
+ assert.ok(sess, 'session saved to store')
+ var exp = new Date(sess.cookie.expires)
+ assert.strictEqual(exp.toUTCString(), expires(res))
+ setTimeout(function () {
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(200, function (err, res) {
+ if (err) return done(err)
+ store.get(id, function (err, sess) {
+ if (err) return done(err)
+ assert.strictEqual(res.text, id)
+ assert.ok(sess, 'session still in store')
+ assert.notStrictEqual(new Date(sess.cookie.expires).toUTCString(), exp.toUTCString(), 'session cookie expiration updated')
+ done()
+ })
+ })
+ }, (1000 - (Date.now() % 1000) + 200))
+ })
})
})
})
@@ -435,19 +428,14 @@ describe('session()', function(){
.expect(shouldSetCookie('connect.sid'))
.expect(200, 'session 1', function (err, res) {
if (err) return done(err)
- var val = sid(res)
- assert.ok(val)
store.clear(function (err) {
if (err) return done(err)
request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(shouldSetCookie('connect.sid'))
- .expect(200, 'session 2', function (err, res) {
- if (err) return done(err)
- assert.notEqual(sid(res), val)
- done()
- })
+ .expect(shouldSetCookieToDifferentSessionId(sid(res)))
+ .expect(200, 'session 2', done)
})
})
})
@@ -473,6 +461,7 @@ describe('session()', function(){
.get('/')
.set('Cookie', 'sessid=' + val)
.expect(shouldSetCookie('sessid'))
+ .expect(shouldSetCookieToDifferentSessionId(val))
.expect(200, 'session created', done)
})
})
@@ -539,18 +528,13 @@ describe('session()', function(){
.expect(shouldSetCookie('connect.sid'))
.expect(200, 'session 1', function (err, res) {
if (err) return done(err)
- var val = sid(res)
- assert.ok(val)
setTimeout(function () {
request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(shouldSetCookie('connect.sid'))
- .expect(200, 'session 2', function (err, res) {
- if (err) return done(err)
- assert.notEqual(sid(res), val)
- done()
- })
+ .expect(shouldSetCookieToDifferentSessionId(sid(res)))
+ .expect(200, 'session 2', done)
}, 15)
})
})
@@ -571,7 +555,7 @@ describe('session()', function(){
setTimeout(function () {
store.all(function (err, sess) {
if (err) return done(err)
- assert.equal(Object.keys(sess).length, 0)
+ assert.strictEqual(Object.keys(sess).length, 0)
done()
})
}, 10)
@@ -579,6 +563,31 @@ describe('session()', function(){
})
})
+ describe('when session without cookie property in store', function () {
+ it('should pass error from inflate', function (done) {
+ var count = 0
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store }, function (req, res) {
+ req.session.num = req.session.num || ++count
+ res.end('session ' + req.session.num)
+ })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, 'session 1', function (err, res) {
+ if (err) return done(err)
+ store.set(sid(res), { foo: 'bar' }, function (err) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(500, /Cannot read prop/, done)
+ })
+ })
+ })
+ })
+
describe('proxy option', function(){
describe('when enabled', function(){
var server
@@ -611,56 +620,66 @@ describe('session()', function(){
})
describe('when disabled', function(){
- var server
before(function () {
- server = createServer({ proxy: false, cookie: { secure: true, maxAge: 5 }})
+ function setup (req) {
+ req.secure = req.headers['x-secure']
+ ? JSON.parse(req.headers['x-secure'])
+ : undefined
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { proxy: false, cookie: { secure: true }}, respond)
})
it('should not trust X-Forwarded-Proto', function(done){
- request(server)
+ request(this.server)
.get('/')
.set('X-Forwarded-Proto', 'https')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
})
- it('should ignore req.secure from express', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', proxy: false, cookie: { secure: true, maxAge: min }}))
- .use(function(req, res) { res.json(req.secure); });
- app.enable('trust proxy');
-
- request(app)
+ it('should ignore req.secure', function (done) {
+ request(this.server)
.get('/')
.set('X-Forwarded-Proto', 'https')
+ .set('X-Secure', 'true')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, 'true', done)
})
})
describe('when unspecified', function(){
- var server
before(function () {
- server = createServer({ cookie: { secure: true, maxAge: 5 }})
+ function setup (req) {
+ req.secure = req.headers['x-secure']
+ ? JSON.parse(req.headers['x-secure'])
+ : undefined
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { cookie: { secure: true }}, respond)
})
it('should not trust X-Forwarded-Proto', function(done){
- request(server)
+ request(this.server)
.get('/')
.set('X-Forwarded-Proto', 'https')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
})
- it('should use req.secure from express', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { secure: true, maxAge: min }}))
- .use(function(req, res) { res.json(req.secure); });
- app.enable('trust proxy');
-
- request(app)
+ it('should use req.secure', function (done) {
+ request(this.server)
.get('/')
.set('X-Forwarded-Proto', 'https')
+ .set('X-Secure', 'true')
.expect(shouldSetCookie('connect.sid'))
.expect(200, 'true', done)
})
@@ -668,6 +687,61 @@ describe('session()', function(){
})
describe('cookie option', function () {
+ describe('when "path" set to "/foo/bar"', function () {
+ before(function () {
+ this.server = createServer({ cookie: { path: '/foo/bar' } })
+ })
+
+ it('should not set cookie for "/" request', function (done) {
+ request(this.server)
+ .get('/')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
+ })
+
+ it('should not set cookie for "http://foo/bar" request', function (done) {
+ request(this.server)
+ .get('/')
+ .set('host', 'http://foo/bar')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
+ })
+
+ it('should set cookie for "/foo/bar" request', function (done) {
+ request(this.server)
+ .get('/foo/bar/baz')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should set cookie for "/foo/bar/baz" request', function (done) {
+ request(this.server)
+ .get('/foo/bar/baz')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ describe('when mounted at "/foo"', function () {
+ before(function () {
+ this.server = createServer(mountAt('/foo'), { cookie: { path: '/foo/bar' } })
+ })
+
+ it('should set cookie for "/foo/bar" request', function (done) {
+ request(this.server)
+ .get('/foo/bar')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should not set cookie for "/foo/foo/bar" request', function (done) {
+ request(this.server)
+ .get('/foo/foo/bar')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
+ })
+ })
+ })
+
describe('when "secure" set to "auto"', function () {
describe('when "proxy" is "true"', function () {
before(function () {
@@ -678,8 +752,7 @@ describe('session()', function(){
request(this.server)
.get('/')
.set('X-Forwarded-Proto', 'https')
- .expect(shouldSetCookie('connect.sid'))
- .expect(shouldSetSecureCookie('connect.sid'))
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
.expect(200, done)
})
})
@@ -693,112 +766,408 @@ describe('session()', function(){
request(this.server)
.get('/')
.set('X-Forwarded-Proto', 'https')
- .expect(shouldSetCookie('connect.sid'))
- .expect(shouldNotSetSecureCookie('connect.sid'))
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
.expect(200, done)
})
})
describe('when "proxy" is undefined', function() {
before(function () {
- this.app = express()
- .use(function(req, res, next) { Object.defineProperty(req, 'secure', { value: JSON.parse(req.headers['x-secure']) }); next(); })
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: min, secure: 'auto' }}))
- .use(function(req, res) { res.json(req.secure); });
+ function setup (req) {
+ req.secure = JSON.parse(req.headers['x-secure'])
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { cookie: { secure: 'auto' } }, respond)
})
it('should set secure if req.secure = true', function (done) {
- request(this.app)
+ request(this.server)
.get('/')
.set('X-Secure', 'true')
- .expect(shouldSetCookie('connect.sid'))
- .expect(shouldSetSecureCookie('connect.sid'))
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
.expect(200, 'true', done)
})
it('should not set secure if req.secure = false', function (done) {
- request(this.app)
+ request(this.server)
.get('/')
.set('X-Secure', 'false')
- .expect(shouldSetCookie('connect.sid'))
- .expect(shouldNotSetSecureCookie('connect.sid'))
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
.expect(200, 'false', done)
})
})
- })
- })
- describe('genid option', function(){
- it('should reject non-function values', function(){
- assert.throws(session.bind(null, { genid: 'bogus!' }), /genid.*must/)
- });
+ describe('when request socket is encrypted', function () {
+ it('should set secure over TLS', function (done) {
+ var cert = fs.readFileSync(__dirname + '/fixtures/server.crt', 'ascii')
+ var server = https.createServer({
+ key: fs.readFileSync(__dirname + '/fixtures/server.key', 'ascii'),
+ cert: cert
+ })
- it('should provide default generator', function(done){
- request(createServer())
- .get('/')
- .expect(shouldSetCookie('connect.sid'))
- .expect(200, done)
- });
+ server.on('request', createRequestListener({ secret: 'keyboard cat', cookie: { secure: 'auto' } }))
- it('should allow custom function', function(done){
- function genid() { return 'apple' }
+ var agent = new https.Agent({ ca: cert })
+ var createConnection = agent.createConnection
- request(createServer({ genid: genid }))
- .get('/')
- .expect(shouldSetCookieToValue('connect.sid', 's%3Aapple.D8Y%2BpkTAmeR0PobOhY4G97PRW%2Bj7bUnP%2F5m6%2FOn1MCU'))
- .expect(200, done)
- });
+ agent.createConnection = function (options) {
+ options.servername = 'express-session.local'
+ return createConnection.call(this, options)
+ }
- it('should encode unsafe chars', function(done){
- function genid() { return '%' }
+ var req = request(server).get('/')
+ req.agent(agent)
+ req.expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
+ req.expect(200, done)
+ })
+ })
+ })
- request(createServer({ genid: genid }))
- .get('/')
- .expect(shouldSetCookieToValue('connect.sid', 's%3A%25.kzQ6x52kKVdF35Qh62AWk4ZekS28K5XYCXKa%2FOTZ01g'))
- .expect(200, done)
- });
+ describe('when "cookie" is a function', function () {
+ it('should call custom function and apply cookie options', function (done) {
+ var cookieCallbackCalled = false;
+ var cookieCallback = function () {
+ cookieCallbackCalled = true;
+ return { path: '/test', httpOnly: true, secure: false };
+ };
+ var server = createServer({ cookie: cookieCallback });
+ request(server)
+ .get('/test')
+ .expect(
+ shouldSetCookieWithAttributeAndValue('connect.sid', 'Path', '/test')
+ )
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'HttpOnly'))
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
+ .expect(200, function (err) {
+ if (err) return done(err);
+ assert.strictEqual(
+ cookieCallbackCalled,
+ true,
+ 'should have called cookie callback'
+ );
+ done();
+ });
+ });
- it('should provide req argument', function(done){
- function genid(req) { return req.url }
+ it('should provide req argument', function (done) {
+ var _path = '/test';
+ var cookieCallbackCalled = false;
+ var cookieCallback = function (req) {
+ cookieCallbackCalled = true;
+ return { path: req.url, httpOnly: true, secure: false };
+ };
+ var server = createServer({ cookie: cookieCallback });
+ request(server)
+ .get(_path)
+ .expect(
+ shouldSetCookieWithAttributeAndValue('connect.sid', 'Path', _path)
+ )
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'HttpOnly'))
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
+ .expect(200, function (err) {
+ if (err) return done(err);
+ assert.strictEqual(
+ cookieCallbackCalled,
+ true,
+ 'should have called cookie callback'
+ );
+ done();
+ });
+ });
- request(createServer({ genid: genid }))
- .get('/foo')
- .expect(shouldSetCookieToValue('connect.sid', 's%3A%2Ffoo.paEKBtAHbV5s1IB8B2zPnzAgYmmnRPIqObW4VRYj%2FMQ'))
- .expect(200, done)
- });
- });
+ it('should not set cookie when request-path does not match returned path', function (done) {
+ var cookieCallback = function () {
+ return { path: '/admin', httpOnly: true, secure: false };
+ };
+ var server = createServer({ cookie: cookieCallback });
+ request(server)
+ .get('/administrator')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done);
+ });
- describe('key option', function(){
- it('should default to "connect.sid"', function(done){
- request(createServer())
- .get('/')
- .expect(shouldSetCookie('connect.sid'))
- .expect(200, done)
- })
+ it('should resolve "secure" and "sameSite" set to "auto"', function (done) {
+ function setup (req) {
+ req.secure = JSON.parse(req.headers['x-secure'])
+ }
- it('should allow overriding', function(done){
- request(createServer({ key: 'session_id' }))
- .get('/')
- .expect(shouldSetCookie('session_id'))
- .expect(200, done)
- })
- })
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
- describe('rolling option', function(){
- it('should default to false', function(done){
- var app = express();
- app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- req.session.user = 'bob';
- res.end();
+ var cookieCallback = function () {
+ return { path: '/', secure: 'auto', sameSite: 'auto' };
+ };
+ var server = createServer(setup, { cookie: cookieCallback }, respond);
+ request(server)
+ .get('/')
+ .set('X-Secure', 'true')
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
+ .expect(
+ shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None')
+ )
+ .expect(200, 'true', done);
});
+ });
- request(app)
+
+ describe('when "sameSite" set to "auto"', function () {
+ describe('basic functionality', function () {
+ before(function () {
+ function setup (req) {
+ req.secure = JSON.parse(req.headers['x-secure'])
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { cookie: { sameSite: 'auto' } }, respond)
+ })
+
+ it('should set SameSite=None for secure connections', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'true')
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None'))
+ .expect(200, 'true', done)
+ })
+
+ it('should set SameSite=Lax for insecure connections', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'false')
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax'))
+ .expect(200, 'false', done)
+ })
+ })
+
+ describe('with proxy settings', function () {
+ describe('when "proxy" is "true"', function () {
+ before(function () {
+ this.server = createServer({ proxy: true, cookie: { sameSite: 'auto' }})
+ })
+
+ it('should set SameSite=None when X-Forwarded-Proto is https', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Forwarded-Proto', 'https')
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None'))
+ .expect(200, done)
+ })
+
+ it('should set SameSite=Lax when X-Forwarded-Proto is http', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Forwarded-Proto', 'http')
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax'))
+ .expect(200, done)
+ })
+ })
+
+ describe('when "proxy" is "false"', function () {
+ before(function () {
+ this.server = createServer({ proxy: false, cookie: { sameSite: 'auto' }})
+ })
+
+ it('should set SameSite=Lax when X-Forwarded-Proto is https', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Forwarded-Proto', 'https')
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax'))
+ .expect(200, done)
+ })
+ })
+ })
+
+ describe('combined with secure auto', function() {
+ describe('when "secure" is "auto"', function () {
+ before(function () {
+ function setup (req) {
+ req.secure = JSON.parse(req.headers['x-secure'])
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { cookie: { secure: 'auto', sameSite: 'auto' } }, respond)
+ })
+
+ it('should set both Secure and SameSite=None when secure', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'true')
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None'))
+ .expect(200, 'true', done)
+ })
+
+ it('should set neither Secure nor SameSite=None when insecure', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'false')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax'))
+ .expect(200, 'false', done)
+ })
+ })
+
+ describe('when "secure" is "false"', function () {
+ before(function () {
+ function setup (req) {
+ req.secure = JSON.parse(req.headers['x-secure'])
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { cookie: { secure: false, sameSite: 'auto' } }, respond)
+ })
+
+ it('should set SameSite=None without Secure when secure', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'true')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None'))
+ .expect(200, 'true', done)
+ })
+
+ it('should set SameSite=Lax without Secure when insecure', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'false')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'Lax'))
+ .expect(200, 'false', done)
+ })
+ })
+
+ describe('when "secure" is "true"', function () {
+ before(function () {
+ function setup (req) {
+ req.secure = JSON.parse(req.headers['x-secure'])
+ }
+
+ function respond (req, res) {
+ res.end(String(req.secure))
+ }
+
+ this.server = createServer(setup, { cookie: { secure: true, sameSite: 'auto' } }, respond)
+ })
+
+ it('should set both Secure and SameSite=None when secure', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'true')
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'SameSite', 'None'))
+ .expect(200, 'true', done)
+ })
+
+ it('should not set cookie when insecure', function (done) {
+ request(this.server)
+ .get('/')
+ .set('X-Secure', 'false')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, 'false', done)
+ })
+ })
+ })
+ })
+ })
+
+ describe('genid option', function(){
+ it('should reject non-function values', function(){
+ assert.throws(session.bind(null, { genid: 'bogus!' }), /genid.*must/)
+ });
+
+ it('should provide default generator', function(done){
+ request(createServer())
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ });
+
+ it('should allow custom function', function(done){
+ function genid() { return 'apple' }
+
+ request(createServer({ genid: genid }))
+ .get('/')
+ .expect(shouldSetCookieToValue('connect.sid', 's%3Aapple.D8Y%2BpkTAmeR0PobOhY4G97PRW%2Bj7bUnP%2F5m6%2FOn1MCU'))
+ .expect(200, done)
+ });
+
+ it('should encode unsafe chars', function(done){
+ function genid() { return '%' }
+
+ request(createServer({ genid: genid }))
+ .get('/')
+ .expect(shouldSetCookieToValue('connect.sid', 's%3A%25.kzQ6x52kKVdF35Qh62AWk4ZekS28K5XYCXKa%2FOTZ01g'))
+ .expect(200, done)
+ });
+
+ it('should provide req argument', function(done){
+ function genid(req) { return req.url }
+
+ request(createServer({ genid: genid }))
+ .get('/foo')
+ .expect(shouldSetCookieToValue('connect.sid', 's%3A%2Ffoo.paEKBtAHbV5s1IB8B2zPnzAgYmmnRPIqObW4VRYj%2FMQ'))
+ .expect(200, done)
+ });
+ });
+
+ describe('key option', function(){
+ it('should default to "connect.sid"', function(done){
+ request(createServer())
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should allow overriding', function(done){
+ request(createServer({ key: 'session_id' }))
+ .get('/')
+ .expect(shouldSetCookie('session_id'))
+ .expect(200, done)
+ })
+ })
+
+ describe('name option', function () {
+ it('should default to "connect.sid"', function (done) {
+ request(createServer())
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should set the cookie name', function (done) {
+ request(createServer({ name: 'session_id' }))
+ .get('/')
+ .expect(shouldSetCookie('session_id'))
+ .expect(200, done)
+ })
+ })
+
+ describe('rolling option', function(){
+ it('should default to false', function(done){
+ var server = createServer(null, function (req, res) {
+ req.session.user = 'bob'
+ res.end()
+ })
+
+ request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function(err, res){
if (err) return done(err);
- request(app)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(shouldNotHaveHeader('Set-Cookie'))
@@ -807,19 +1176,17 @@ describe('session()', function(){
});
it('should force cookie on unmodified session', function(done){
- var app = express();
- app.use(session({ rolling: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- req.session.user = 'bob';
- res.end();
- });
+ var server = createServer({ rolling: true }, function (req, res) {
+ req.session.user = 'bob'
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function(err, res){
if (err) return done(err);
- request(app)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(shouldSetCookie('connect.sid'))
@@ -828,66 +1195,37 @@ describe('session()', function(){
});
it('should not force cookie on uninitialized session if saveUninitialized option is set to false', function(done){
- var count = 0;
- var app = express();
- app.use(session({ rolling: true, saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, rolling: true, saveUninitialized: false })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '0')
+ .expect(shouldNotSetSessionInStore(store))
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
});
it('should force cookie and save uninitialized session if saveUninitialized option is set to true', function(done){
- var count = 0;
- var app = express();
- app.use(session({ rolling: true, saveUninitialized: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, rolling: true, saveUninitialized: true })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '1')
+ .expect(shouldSetSessionInStore(store))
.expect(shouldSetCookie('connect.sid'))
.expect(200, done)
});
it('should force cookie and save modified session even if saveUninitialized option is set to false', function(done){
- var count = 0;
- var app = express();
- app.use(session({ rolling: true, saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.count = count;
- req.session.user = 'bob';
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, rolling: true, saveUninitialized: false }, function (req, res) {
+ req.session.user = 'bob'
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '1')
+ .expect(shouldSetSessionInStore(store))
.expect(shouldSetCookie('connect.sid'))
.expect(200, done);
});
@@ -895,231 +1233,203 @@ describe('session()', function(){
describe('resave option', function(){
it('should default to true', function(done){
- var count = 0;
- var app = express();
- app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.user = 'bob';
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store }, function (req, res) {
+ req.session.user = 'bob'
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '1')
+ .expect(shouldSetSessionInStore(store))
.expect(200, function(err, res){
if (err) return done(err);
- request(app)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
- .expect('x-count', '2')
+ .expect(shouldSetSessionInStore(store))
.expect(200, done);
});
});
- it('should force save on unmodified session', function(done){
- var count = 0;
- var app = express();
- app.use(session({ resave: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.user = 'bob';
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ describe('when true', function () {
+ it('should force save on unmodified session', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, resave: true }, function (req, res) {
+ req.session.user = 'bob'
+ res.end()
+ })
- request(app)
- .get('/')
- .expect('x-count', '1')
- .expect(200, function(err, res){
- if (err) return done(err);
- request(app)
+ request(server)
.get('/')
- .set('Cookie', cookie(res))
- .expect('x-count', '2')
- .expect(200, done);
- });
- });
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, done)
+ })
+ })
+ })
- it('should prevent save on unmodified session', function(done){
- var count = 0;
- var app = express();
- app.use(session({ resave: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.user = 'bob';
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ describe('when false', function () {
+ it('should prevent save on unmodified session', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, resave: false }, function (req, res) {
+ req.session.user = 'bob'
+ res.end()
+ })
- request(app)
- .get('/')
- .expect('x-count', '1')
- .expect(200, function(err, res){
- if (err) return done(err);
- request(app)
+ request(server)
.get('/')
- .set('Cookie', cookie(res))
- .expect('x-count', '1')
- .expect(200, done);
- });
- });
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(shouldNotSetSessionInStore(store))
+ .expect(200, done)
+ })
+ })
- it('should still save modified session', function(done){
- var count = 0;
- var app = express();
- app.use(session({ resave: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.count = count;
- req.session.user = 'bob';
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ it('should still save modified session', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ resave: false, store: store }, function (req, res) {
+ if (req.method === 'PUT') {
+ req.session.token = req.url.slice(1)
+ }
+ res.end('token=' + (req.session.token || ''))
+ })
- request(app)
- .get('/')
- .expect('x-count', '1')
- .expect(200, function(err, res){
- if (err) return done(err);
- request(app)
- .get('/')
- .set('Cookie', cookie(res))
- .expect('x-count', '2')
- .expect(200, done);
- });
- });
+ request(server)
+ .put('/w6RHhwaA')
+ .expect(200)
+ .expect(shouldSetSessionInStore(store))
+ .expect('token=w6RHhwaA')
+ .end(function (err, res) {
+ if (err) return done(err)
+ var sess = cookie(res)
+ request(server)
+ .get('/')
+ .set('Cookie', sess)
+ .expect(200)
+ .expect(shouldNotSetSessionInStore(store))
+ .expect('token=w6RHhwaA')
+ .end(function (err) {
+ if (err) return done(err)
+ request(server)
+ .put('/zfQ3rzM3')
+ .set('Cookie', sess)
+ .expect(200)
+ .expect(shouldSetSessionInStore(store))
+ .expect('token=zfQ3rzM3')
+ .end(done)
+ })
+ })
+ })
- it('should pass session touch error', function (done) {
- var cb = after(2, done)
- var store = new session.MemoryStore()
- var server = createServer({ store: store, resave: false }, function (req, res) {
- req.session.hit = true
- res.end('session saved')
+ it('should detect a "cookie" property as modified', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, resave: false }, function (req, res) {
+ req.session.user = req.session.user || {}
+ req.session.user.name = 'bob'
+ req.session.user.cookie = req.session.user.cookie || 0
+ req.session.user.cookie++
+ res.end()
+ })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, done)
+ })
})
- store.touch = function touch(sid, sess, callback) {
- callback(new Error('boom!'))
- }
+ it('should pass session touch error', function (done) {
+ var cb = after(2, done)
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, resave: false }, function (req, res) {
+ req.session.hit = true
+ res.end('session saved')
+ })
- server.on('error', function onerror(err) {
- assert.ok(err)
- assert.equal(err.message, 'boom!')
- cb()
- })
+ store.touch = function touch (sid, sess, callback) {
+ callback(new Error('boom!'))
+ }
+
+ server.on('error', function onerror (err) {
+ assert.ok(err)
+ assert.strictEqual(err.message, 'boom!')
+ cb()
+ })
- request(server)
- .get('/')
- .expect(200, 'session saved', function (err, res) {
- if (err) return cb(err)
request(server)
.get('/')
- .set('Cookie', cookie(res))
- .end(cb)
+ .expect(200, 'session saved', function (err, res) {
+ if (err) return cb(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .end(cb)
+ })
})
})
});
describe('saveUninitialized option', function(){
it('should default to true', function(done){
- var count = 0;
- var app = express();
- app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '1')
+ .expect(shouldSetSessionInStore(store))
.expect(shouldSetCookie('connect.sid'))
.expect(200, done);
});
it('should force save of uninitialized session', function(done){
- var count = 0;
- var app = express();
- app.use(session({ saveUninitialized: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, saveUninitialized: true })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '1')
+ .expect(shouldSetSessionInStore(store))
.expect(shouldSetCookie('connect.sid'))
.expect(200, done);
});
it('should prevent save of uninitialized session', function(done){
- var count = 0;
- var app = express();
- app.use(session({ saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, saveUninitialized: false })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '0')
+ .expect(shouldNotSetSessionInStore(store))
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, done)
});
it('should still save modified session', function(done){
- var count = 0;
- var app = express();
- app.use(session({ saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
- app.use(function(req, res, next){
- var save = req.session.save;
- res.setHeader('x-count', count);
- req.session.count = count;
- req.session.user = 'bob';
- req.session.save = function(fn){
- res.setHeader('x-count', ++count);
- return save.call(this, fn);
- };
- res.end();
- });
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, saveUninitialized: false }, function (req, res) {
+ req.session.count = req.session.count || 0
+ req.session.count++
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
- .expect('x-count', '1')
+ .expect(shouldSetSessionInStore(store))
.expect(shouldSetCookie('connect.sid'))
.expect(200, done);
});
@@ -1137,7 +1447,7 @@ describe('session()', function(){
server.on('error', function onerror(err) {
assert.ok(err)
- assert.equal(err.message, 'boom!')
+ assert.strictEqual(err.message, 'boom!')
cb()
})
@@ -1168,6 +1478,50 @@ describe('session()', function(){
assert.throws(createServer.bind(null, { secret: [] }), /secret option array/);
})
+ it('should sign and unsign with a string', function (done) {
+ var server = createServer({ secret: 'awesome cat' }, function (req, res) {
+ if (!req.session.user) {
+ req.session.user = 'bob'
+ res.end('set')
+ } else {
+ res.end('get:' + JSON.stringify(req.session.user))
+ }
+ })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, 'set', function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(200, 'get:"bob"', done)
+ })
+ })
+
+ it('should sign and unsign with a Buffer', function (done) {
+ var server = createServer({ secret: crypto.randomBytes(32) }, function (req, res) {
+ if (!req.session.user) {
+ req.session.user = 'bob'
+ res.end('set')
+ } else {
+ res.end('get:' + JSON.stringify(req.session.user))
+ }
+ })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, 'set', function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(200, 'get:"bob"', done)
+ })
+ })
+
describe('when an array', function () {
it('should sign cookies', function (done) {
var server = createServer({ secret: ['keyboard cat', 'nyan cat'] }, function (req, res) {
@@ -1238,30 +1592,28 @@ describe('session()', function(){
it('should default to keep', function(done){
var store = new session.MemoryStore();
- var app = express()
- .use(session({ store: store, secret: 'keyboard cat' }))
- .use(function(req, res, next){
- req.session.count = req.session.count || 0;
- req.session.count++;
- if (req.session.count === 2) req.session = null;
- res.end();
- });
+ var server = createServer({ store: store }, function (req, res) {
+ req.session.count = req.session.count || 0
+ req.session.count++
+ if (req.session.count === 2) req.session = null
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.expect(200, function(err, res){
if (err) return done(err);
store.length(function(err, len){
if (err) return done(err);
- assert.equal(len, 1)
- request(app)
+ assert.strictEqual(len, 1)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(200, function(err, res){
if (err) return done(err);
store.length(function(err, len){
if (err) return done(err);
- assert.equal(len, 1)
+ assert.strictEqual(len, 1)
done();
});
});
@@ -1271,30 +1623,28 @@ describe('session()', function(){
it('should allow destroy on req.session = null', function(done){
var store = new session.MemoryStore();
- var app = express()
- .use(session({ store: store, unset: 'destroy', secret: 'keyboard cat' }))
- .use(function(req, res, next){
- req.session.count = req.session.count || 0;
- req.session.count++;
- if (req.session.count === 2) req.session = null;
- res.end();
- });
+ var server = createServer({ store: store, unset: 'destroy' }, function (req, res) {
+ req.session.count = req.session.count || 0
+ req.session.count++
+ if (req.session.count === 2) req.session = null
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.expect(200, function(err, res){
if (err) return done(err);
store.length(function(err, len){
if (err) return done(err);
- assert.equal(len, 1)
- request(app)
+ assert.strictEqual(len, 1)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(200, function(err, res){
if (err) return done(err);
store.length(function(err, len){
if (err) return done(err);
- assert.equal(len, 0)
+ assert.strictEqual(len, 0)
done();
});
});
@@ -1304,21 +1654,19 @@ describe('session()', function(){
it('should not set cookie if initial session destroyed', function(done){
var store = new session.MemoryStore();
- var app = express()
- .use(session({ store: store, unset: 'destroy', secret: 'keyboard cat' }))
- .use(function(req, res, next){
- req.session = null;
- res.end();
- });
+ var server = createServer({ store: store, unset: 'destroy' }, function (req, res) {
+ req.session = null
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.expect(shouldNotHaveHeader('Set-Cookie'))
.expect(200, function(err, res){
if (err) return done(err);
store.length(function(err, len){
if (err) return done(err);
- assert.equal(len, 0)
+ assert.strictEqual(len, 0)
done();
});
});
@@ -1338,7 +1686,7 @@ describe('session()', function(){
server.on('error', function onerror(err) {
assert.ok(err)
- assert.equal(err.message, 'boom!')
+ assert.strictEqual(err.message, 'boom!')
cb()
})
@@ -1350,36 +1698,69 @@ describe('session()', function(){
describe('res.end patch', function () {
it('should correctly handle res.end/res.write patched prior', function (done) {
- var app = express()
+ function setup (req, res) {
+ utils.writePatch(res)
+ }
- app.use(writePatch())
- app.use(createSession())
- app.use(function (req, res) {
+ function respond (req, res) {
req.session.hit = true
res.write('hello, ')
res.end('world')
- })
+ }
- request(app)
+ request(createServer(setup, null, respond))
.get('/')
.expect(200, 'hello, world', done)
})
it('should correctly handle res.end/res.write patched after', function (done) {
- var app = express()
-
- app.use(createSession())
- app.use(writePatch())
- app.use(function (req, res) {
+ function respond (req, res) {
+ utils.writePatch(res)
req.session.hit = true
res.write('hello, ')
res.end('world')
- })
+ }
- request(app)
+ request(createServer(null, respond))
.get('/')
.expect(200, 'hello, world', done)
})
+
+ it('should error when res.end is called twice', function (done) {
+ var error1 = null
+ var error2 = null
+ var server = http.createServer(function (req, res) {
+ res.end()
+
+ try {
+ res.setHeader('Content-Length', '3')
+ res.end('foo')
+ } catch (e) {
+ error1 = e
+ }
+ })
+
+ function respond (req, res) {
+ res.end()
+
+ try {
+ res.setHeader('Content-Length', '3')
+ res.end('foo')
+ } catch (e) {
+ error2 = e
+ }
+ }
+
+ request(server)
+ .get('/')
+ .end(function (err, res) {
+ if (err) return done(err)
+ request(createServer(null, respond))
+ .get('/')
+ .expect(function () { assert.strictEqual((error1 && error1.message), (error2 && error2.message)) })
+ .expect(res.statusCode, res.text, done)
+ })
+ })
})
describe('req.session', function(){
@@ -1408,22 +1789,19 @@ describe('session()', function(){
it('should only set-cookie when modified', function(done){
var modify = true;
+ var server = createServer(null, function (req, res) {
+ if (modify) {
+ req.session.count = req.session.count || 0
+ req.session.count++
+ }
+ res.end(req.session.count.toString())
+ })
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
- .use(function(req, res, next){
- if (modify) {
- req.session.count = req.session.count || 0;
- req.session.count++;
- }
- res.end(req.session.count.toString());
- });
-
- request(app)
+ request(server)
.get('/')
.expect(200, '1', function (err, res) {
if (err) return done(err)
- request(app)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(200, '2', function (err, res) {
@@ -1431,7 +1809,7 @@ describe('session()', function(){
var val = cookie(res);
modify = false;
- request(app)
+ request(server)
.get('/')
.set('Cookie', val)
.expect(shouldNotHaveHeader('Set-Cookie'))
@@ -1439,7 +1817,7 @@ describe('session()', function(){
if (err) return done(err)
modify = true;
- request(app)
+ request(server)
.get('/')
.set('Cookie', val)
.expect(shouldSetCookie('connect.sid'))
@@ -1450,19 +1828,17 @@ describe('session()', function(){
})
it('should not have enumerable methods', function (done) {
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
- .use(function(req, res, next) {
- req.session.foo = 'foo';
- req.session.bar = 'bar';
- var keys = [];
- for (var key in req.session) {
- keys.push(key);
- }
- res.end(keys.sort().join(','));
- });
+ var server = createServer(null, function (req, res) {
+ req.session.foo = 'foo'
+ req.session.bar = 'bar'
+ var keys = []
+ for (var key in req.session) {
+ keys.push(key)
+ }
+ res.end(keys.sort().join(','))
+ })
- request(app)
+ request(server)
.get('/')
.expect(200, 'bar,cookie,foo', done);
});
@@ -1505,51 +1881,41 @@ describe('session()', function(){
describe('.destroy()', function(){
it('should destroy the previous session', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat' }))
- .use(function(req, res, next){
- req.session.destroy(function(err){
- if (err) return next(err)
- assert(!req.session, 'req.session after destroy');
- res.end();
- });
- });
+ var server = createServer(null, function (req, res) {
+ req.session.destroy(function (err) {
+ if (err) res.statusCode = 500
+ res.end(String(req.session))
+ })
+ })
- request(app)
+ request(server)
.get('/')
.expect(shouldNotHaveHeader('Set-Cookie'))
- .expect(200, done)
+ .expect(200, 'undefined', done)
})
})
describe('.regenerate()', function(){
it('should destroy/replace the previous session', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
- .use(function(req, res, next){
- var id = req.session.id;
- req.session.regenerate(function(err){
- if (err) return next(err)
- assert.notEqual(id, req.session.id)
- res.end();
- });
- });
+ var server = createServer(null, function (req, res) {
+ var id = req.session.id
+ req.session.regenerate(function (err) {
+ if (err) res.statusCode = 500
+ res.end(String(req.session.id === id))
+ })
+ })
- request(app)
+ request(server)
.get('/')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function (err, res) {
if (err) return done(err)
- var id = sid(res)
- request(app)
+ request(server)
.get('/')
.set('Cookie', cookie(res))
.expect(shouldSetCookie('connect.sid'))
- .expect(200, function (err, res) {
- if (err) return done(err)
- assert.notEqual(sid(res), id)
- done();
- });
+ .expect(shouldSetCookieToDifferentSessionId(sid(res)))
+ .expect(200, 'false', done)
});
})
})
@@ -1623,6 +1989,49 @@ describe('session()', function(){
.expect(500, 'failed to load session', done)
})
})
+
+ it('should not override an overridden `reload` in case of errors', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store, resave: false }, function (req, res) {
+ if (req.url === '/') {
+ req.session.active = true
+ res.end('session created')
+ return
+ }
+
+ store.clear(function (err) {
+ if (err) return done(err)
+
+ // reload way too many times on top of each other,
+ // attempting to overflow the call stack
+ var iters = 20
+ reload()
+ function reload () {
+ if (!--iters) {
+ res.end('ok')
+ return
+ }
+
+ try {
+ req.session.reload(reload)
+ } catch (e) {
+ res.statusCode = 500
+ res.end(e.message)
+ }
+ }
+ })
+ })
+
+ request(server)
+ .get('/')
+ .expect(200, 'session created', function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/foo')
+ .set('Cookie', cookie(res))
+ .expect(200, 'ok', done)
+ })
+ })
})
describe('.save()', function () {
@@ -1645,7 +2054,6 @@ describe('session()', function(){
})
it('should prevent end-of-request save', function (done) {
- var count = 0
var store = new session.MemoryStore()
var server = createServer({ store: store }, function (req, res) {
req.session.hit = true
@@ -1655,26 +2063,66 @@ describe('session()', function(){
})
})
- var _set = store.set
- store.set = function set(sid, sess, callback) {
- count++
- _set.call(store, sid, sess, callback)
- }
+ request(server)
+ .get('/')
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, 'saved', function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, 'saved', done)
+ })
+ })
+
+ it('should prevent end-of-request save on reloaded session', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ store: store }, function (req, res) {
+ req.session.hit = true
+ req.session.reload(function () {
+ req.session.save(function (err) {
+ if (err) return res.end(err.message)
+ res.end('saved')
+ })
+ })
+ })
request(server)
.get('/')
+ .expect(shouldSetSessionInStore(store))
.expect(200, 'saved', function (err, res) {
if (err) return done(err)
- assert.equal(count, 1)
- count = 0
request(server)
.get('/')
.set('Cookie', cookie(res))
- .expect(200, 'saved', function (err) {
- if (err) return done(err)
- assert.equal(count, 1)
- done()
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, 'saved', done)
+ })
+ })
+
+ describe('when saveUninitialized is false', function () {
+ it('should prevent end-of-request save', function (done) {
+ var store = new session.MemoryStore()
+ var server = createServer({ saveUninitialized: false, store: store }, function (req, res) {
+ req.session.hit = true
+ req.session.save(function (err) {
+ if (err) return res.end(err.message)
+ res.end('saved')
+ })
})
+
+ request(server)
+ .get('/')
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, 'saved', function (err, res) {
+ if (err) return done(err)
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(shouldSetSessionInStore(store))
+ .expect(200, 'saved', done)
+ })
})
})
})
@@ -1704,7 +2152,7 @@ describe('session()', function(){
if (err) return done(err);
store.get(id, function (err, sess) {
if (err) return done(err)
- assert.notEqual(new Date(sess.cookie.expires).getTime(), exp.getTime())
+ assert.notStrictEqual(new Date(sess.cookie.expires).getTime(), exp.getTime())
done()
})
})
@@ -1717,56 +2165,36 @@ describe('session()', function(){
describe('.cookie', function(){
describe('.*', function(){
it('should serialize as parameters', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', proxy: true, cookie: { maxAge: min }}))
- .use(function(req, res, next){
- req.session.cookie.httpOnly = false;
- req.session.cookie.secure = true;
- res.end();
- });
+ var server = createServer({ proxy: true }, function (req, res) {
+ req.session.cookie.httpOnly = false
+ req.session.cookie.secure = true
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.set('X-Forwarded-Proto', 'https')
- .expect(200, function(err, res){
- if (err) return done(err);
- var val = cookie(res);
- assert.equal(val.indexOf('HttpOnly'), -1, 'should not be HttpOnly cookie')
- assert.notEqual(val.indexOf('Secure'), -1, 'should be Secure cookie')
- done();
- });
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'HttpOnly'))
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Secure'))
+ .expect(200, done)
})
-
- it('should default to a browser-session length cookie', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/admin' }}))
- .use(function(req, res, next){
- res.end();
- });
-
- request(app)
- .get('/admin')
- .expect(200, function(err, res){
- if (err) return done(err);
- var val = cookie(res);
- assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires')
- done();
- });
+
+ it('should default to a browser-session length cookie', function(done){
+ request(createServer({ cookie: { path: '/admin' } }))
+ .get('/admin')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires'))
+ .expect(200, done)
})
it('should Set-Cookie only once for browser-session cookies', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/admin' }}))
- .use(function(req, res, next){
- res.end();
- });
+ var server = createServer({ cookie: { path: '/admin' } })
- request(app)
+ request(server)
.get('/admin/foo')
.expect(shouldSetCookie('connect.sid'))
.expect(200, function (err, res) {
if (err) return done(err)
- request(app)
+ request(server)
.get('/admin')
.set('Cookie', cookie(res))
.expect(shouldNotHaveHeader('Set-Cookie'))
@@ -1775,40 +2203,151 @@ describe('session()', function(){
})
it('should override defaults', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/admin', httpOnly: false, secure: true, maxAge: 5000 }}))
- .use(function(req, res, next){
- req.session.cookie.secure = false;
- res.end();
- });
+ var opts = {
+ httpOnly: false,
+ maxAge: 5000,
+ path: '/admin',
+ priority: 'high',
+ secure: true
+ }
+ var server = createServer({ cookie: opts }, function (req, res) {
+ req.session.cookie.secure = false
+ res.end()
+ })
- request(app)
- .get('/admin')
- .expect(200, function(err, res){
- if (err) return done(err);
- var val = cookie(res);
- assert.equal(val.indexOf('HttpOnly'), -1, 'should not be HttpOnly cookie')
- assert.equal(val.indexOf('Secure'), -1, 'should not be Secure cookie')
- assert.notEqual(val.indexOf('Path=/admin'), -1, 'should have cookie path /admin')
- assert.notEqual(val.indexOf('Expires'), -1, 'should have cookie Expires')
- done();
- });
+ request(server)
+ .get('/admin')
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Expires'))
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'HttpOnly'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'Path', '/admin'))
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Secure'))
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'Priority', 'High'))
+ .expect(200, done)
+ })
+
+ it('should forward errors setting cookie', function (done) {
+ var cb = after(2, done)
+ var server = createServer({ cookie: { expires: new Date(NaN) } }, function (req, res) {
+ res.end()
+ })
+
+ server.on('error', function onerror (err) {
+ assert.ok(err)
+ assert.strictEqual(err.message, 'option expires is invalid')
+ cb()
+ })
+
+ request(server)
+ .get('/admin')
+ .expect(200, cb)
})
it('should preserve cookies set before writeHead is called', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat' }))
- .use(function(req, res, next){
- var cookie = new Cookie();
- res.setHeader('Set-Cookie', cookie.serialize('previous', 'cookieValue'));
- res.end();
- });
+ var server = createServer(null, function (req, res) {
+ var cookie = new Cookie()
+ res.setHeader('Set-Cookie', cookie.serialize('previous', 'cookieValue'))
+ res.end()
+ })
- request(app)
+ request(server)
.get('/')
.expect(shouldSetCookieToValue('previous', 'cookieValue'))
.expect(200, done)
})
+
+ it('should preserve cookies set in writeHead', function (done) {
+ var server = createServer(null, function (req, res) {
+ var cookie = new Cookie()
+ res.writeHead(200, {
+ 'Set-Cookie': cookie.serialize('previous', 'cookieValue')
+ })
+ res.end()
+ })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookieToValue('previous', 'cookieValue'))
+ .expect(200, done)
+ })
+ })
+
+ describe('.originalMaxAge', function () {
+ it('should equal original maxAge', function (done) {
+ var server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) {
+ res.end(JSON.stringify(req.session.cookie.originalMaxAge))
+ })
+
+ request(server)
+ .get('/')
+ .expect(200)
+ .expect(function (res) {
+ // account for 1ms latency
+ assert.ok(res.text === '2000' || res.text === '1999',
+ 'expected 2000, got ' + res.text)
+ })
+ .end(done)
+ })
+
+ it('should equal original maxAge for all requests', function (done) {
+ var server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) {
+ res.end(JSON.stringify(req.session.cookie.originalMaxAge))
+ })
+
+ request(server)
+ .get('/')
+ .expect(200)
+ .expect(function (res) {
+ // account for 1ms latency
+ assert.ok(res.text === '2000' || res.text === '1999',
+ 'expected 2000, got ' + res.text)
+ })
+ .end(function (err, res) {
+ if (err) return done(err)
+ setTimeout(function () {
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(200)
+ .expect(function (res) {
+ // account for 1ms latency
+ assert.ok(res.text === '2000' || res.text === '1999',
+ 'expected 2000, got ' + res.text)
+ })
+ .end(done)
+ }, 100)
+ })
+ })
+
+ it('should equal original maxAge for all requests', function (done) {
+ var store = new SmartStore()
+ var server = createServer({ cookie: { maxAge: 2000 }, store: store }, function (req, res) {
+ res.end(JSON.stringify(req.session.cookie.originalMaxAge))
+ })
+
+ request(server)
+ .get('/')
+ .expect(200)
+ .expect(function (res) {
+ // account for 1ms latency
+ assert.ok(res.text === '2000' || res.text === '1999',
+ 'expected 2000, got ' + res.text)
+ })
+ .end(function (err, res) {
+ if (err) return done(err)
+ setTimeout(function () {
+ request(server)
+ .get('/')
+ .set('Cookie', cookie(res))
+ .expect(200)
+ .expect(function (res) {
+ // account for 1ms latency
+ assert.ok(res.text === '2000' || res.text === '1999',
+ 'expected 2000, got ' + res.text)
+ })
+ .end(done)
+ }, 100)
+ })
+ })
})
describe('.secure', function(){
@@ -1851,175 +2390,87 @@ describe('session()', function(){
})
})
- describe('when the pathname does not match cookie.path', function(){
- it('should not set-cookie', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
- .use(function(req, res, next){
- if (!req.session) {
- return res.end();
- }
- req.session.foo = Math.random();
- res.end();
- });
+ describe('.maxAge', function () {
+ before(function (done) {
+ var ctx = this
+
+ ctx.cookie = ''
+ ctx.server = createServer({ cookie: { maxAge: 2000 } }, function (req, res) {
+ switch (++req.session.count) {
+ case 1:
+ break
+ case 2:
+ req.session.cookie.maxAge = 5000
+ break
+ case 3:
+ req.session.cookie.maxAge = 3000000000
+ break
+ default:
+ req.session.count = 0
+ break
+ }
+ res.end(req.session.count.toString())
+ })
- request(app)
+ request(ctx.server)
.get('/')
- .expect(shouldNotHaveHeader('Set-Cookie'))
- .expect(200, done)
+ .end(function (err, res) {
+ ctx.cookie = res && cookie(res)
+ done(err)
+ })
})
- it('should not set-cookie even for FQDN', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
- .use(function(req, res, next){
- if (!req.session) {
- return res.end();
- }
-
- req.session.foo = Math.random();
- res.end();
- });
-
- request(app)
+ it('should set cookie expires relative to maxAge', function (done) {
+ request(this.server)
.get('/')
- .set('host', 'http://foo/bar')
- .expect(shouldNotHaveHeader('Set-Cookie'))
- .expect(200, done)
- })
- })
-
- describe('when the pathname does match cookie.path', function(){
- it('should set-cookie', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
- .use(function(req, res, next){
- req.session.foo = Math.random();
- res.end();
- });
-
- request(app)
- .get('/foo/bar/baz')
- .expect(shouldSetCookie('connect.sid'))
- .expect(200, done)
- })
-
- it('should set-cookie even for FQDN', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
- .use(function(req, res, next){
- req.session.foo = Math.random();
- res.end();
- });
-
- request(app)
- .get('/foo/bar/baz')
- .set('host', 'http://example.com')
- .expect(shouldSetCookie('connect.sid'))
- .expect(200, done)
+ .set('Cookie', this.cookie)
+ .expect(shouldSetCookieToExpireIn('connect.sid', 2000))
+ .expect(200, '1', done)
})
- })
-
- describe('.maxAge', function(){
- var val;
- var app = express()
- .use(session({ secret: 'keyboard cat', cookie: { maxAge: 2000 }}))
- .use(function(req, res, next){
- req.session.count = req.session.count || 0;
- req.session.count++;
- if (req.session.count == 2) req.session.cookie.maxAge = 5000;
- if (req.session.count == 3) req.session.cookie.maxAge = 3000000000;
- res.end(req.session.count.toString());
- });
-
- it('should set relative in milliseconds', function(done){
- request(app)
- .get('/')
- .expect(200, '1', function (err, res) {
- if (err) return done(err)
- var a = new Date(expires(res))
- var b = new Date
- var delta = a.valueOf() - b.valueOf()
-
- val = cookie(res);
-
- assert.ok(delta > 1000 && delta <= 2000)
- done();
- });
- });
- it('should modify cookie when changed', function(done){
- request(app)
+ it('should modify cookie expires when changed', function (done) {
+ request(this.server)
.get('/')
- .set('Cookie', val)
- .expect(200, '2', function (err, res) {
- if (err) return done(err)
- var a = new Date(expires(res))
- var b = new Date
- var delta = a.valueOf() - b.valueOf()
-
- val = cookie(res);
-
- assert.ok(delta > 4000 && delta <= 5000)
- done();
- });
- });
+ .set('Cookie', this.cookie)
+ .expect(shouldSetCookieToExpireIn('connect.sid', 5000))
+ .expect(200, '2', done)
+ })
- it('should modify cookie when changed to large value', function(done){
- request(app)
+ it('should modify cookie expires when changed to large value', function (done) {
+ request(this.server)
.get('/')
- .set('Cookie', val)
- .expect(200, '3', function (err, res) {
- if (err) return done(err)
- var a = new Date(expires(res))
- var b = new Date
- var delta = a.valueOf() - b.valueOf()
-
- val = cookie(res);
-
- assert.ok(delta > 2999999000 && delta <= 3000000000)
- done();
- });
- });
+ .set('Cookie', this.cookie)
+ .expect(shouldSetCookieToExpireIn('connect.sid', 3000000000))
+ .expect(200, '3', done)
+ })
})
describe('.expires', function(){
describe('when given a Date', function(){
it('should set absolute', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat' }))
- .use(function(req, res, next){
- req.session.cookie.expires = new Date(0);
- res.end();
- });
-
- request(app)
+ var server = createServer(null, function (req, res) {
+ req.session.cookie.expires = new Date(0)
+ res.end()
+ })
+
+ request(server)
.get('/')
- .end(function(err, res){
- if (err) return done(err)
- assert.equal(expires(res), 'Thu, 01 Jan 1970 00:00:00 GMT')
- done();
- });
+ .expect(shouldSetCookieWithAttributeAndValue('connect.sid', 'Expires', 'Thu, 01 Jan 1970 00:00:00 GMT'))
+ .expect(200, done)
})
})
describe('when null', function(){
it('should be a browser-session cookie', function(done){
- var app = express()
- .use(session({ secret: 'keyboard cat' }))
- .use(function(req, res, next){
- req.session.cookie.expires = null;
- res.end();
- });
-
- request(app)
+ var server = createServer(null, function (req, res) {
+ req.session.cookie.expires = null
+ res.end()
+ })
+
+ request(server)
.get('/')
- .expect(200, function(err, res){
- if (err) return done(err);
- var val = cookie(res);
- assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires')
- done();
- });
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires'))
+ .expect(200, done)
})
it('should not reset cookie', function (done) {
@@ -2030,18 +2481,14 @@ describe('session()', function(){
request(server)
.get('/')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires'))
.expect(200, function (err, res) {
if (err) return done(err);
- var val = cookie(res);
- assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires')
request(server)
.get('/')
- .set('Cookie', val)
- .expect(200, function (err, res) {
- if (err) return done(err);
- assert.ok(!cookie(res));
- done();
- });
+ .set('Cookie', cookie(res))
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
});
})
@@ -2054,22 +2501,53 @@ describe('session()', function(){
request(server)
.get('/')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Expires'))
.expect(200, function (err, res) {
if (err) return done(err);
- var val = cookie(res);
- assert.equal(val.indexOf('Expires'), -1, 'should be not have cookie Expires')
request(server)
.get('/')
- .set('Cookie', val)
- .expect(200, function (err, res) {
- if (err) return done(err);
- assert.ok(!cookie(res));
- done();
- });
+ .set('Cookie', cookie(res))
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
});
})
})
})
+
+ describe('.partitioned', function () {
+ describe('by default', function () {
+ it('should not set partitioned attribute', function (done) {
+ var server = createServer()
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Partitioned'))
+ .expect(200, done)
+ })
+ })
+
+ describe('when "false"', function () {
+ it('should not set partitioned attribute', function (done) {
+ var server = createServer({ cookie: { partitioned: false } })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookieWithoutAttribute('connect.sid', 'Partitioned'))
+ .expect(200, done)
+ })
+ })
+
+ describe('when "true"', function () {
+ it('should set partitioned attribute', function (done) {
+ var server = createServer({ cookie: { partitioned: true } })
+
+ request(server)
+ .get('/')
+ .expect(shouldSetCookieWithAttribute('connect.sid', 'Partitioned'))
+ .expect(200, done)
+ })
+ })
+ })
})
})
@@ -2116,7 +2594,7 @@ describe('session()', function(){
var app = express()
.use(cookieParser())
.use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() })
- .use(session({ secret: 'keyboard cat' }))
+ .use(createSession())
.use(function(req, res, next){
req.session.count = req.session.count || 0
req.session.count++
@@ -2138,7 +2616,7 @@ describe('session()', function(){
var app = express()
.use(cookieParser())
.use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() })
- .use(session({ secret: 'keyboard cat', key: 'sessid' }))
+ .use(createSession({ key: 'sessid' }))
.use(function(req, res, next){
req.session.count = req.session.count || 0
req.session.count++
@@ -2160,7 +2638,7 @@ describe('session()', function(){
var app = express()
.use(cookieParser())
.use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() })
- .use(session({ secret: 'keyboard cat', key: 'sessid' }))
+ .use(createSession({ key: 'sessid' }))
.use(function(req, res, next){
req.session.count = req.session.count || 0
req.session.count++
@@ -2183,7 +2661,7 @@ describe('session()', function(){
var app = express()
.use(cookieParser('keyboard cat'))
.use(function(req, res, next){ delete req.headers.cookie; next() })
- .use(session())
+ .use(createSession())
.use(function(req, res, next){
req.session.count = req.session.count || 0
req.session.count++
@@ -2203,13 +2681,134 @@ describe('session()', function(){
})
})
+describe('path matching (RFC 6265)', function () {
+ describe('when "path" is "/" (root path)', function () {
+ before(function () {
+ this.server = createServer({ cookie: { path: '/' } })
+ })
+
+ it('should set cookie when request-path is "/" (root path)', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path and the request-path are identical."
+ request(this.server)
+ .get('/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should set cookie when request-path is any path ("/foo")', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
+ // character of the cookie-path is %x2F ("/")."
+ request(this.server)
+ .get('/foo')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should set cookie when request-path has multiple segments ("/foo/bar/baz")', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
+ // character of the cookie-path is %x2F ("/")."
+ request(this.server)
+ .get('/foo/bar/baz')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+ })
+
+ describe('when "path" is "/admin"', function () {
+ before(function () {
+ this.server = createServer({ cookie: { path: '/admin' } })
+ })
+
+ it('should set cookie when request-path and cookie-path are identical ("/admin")', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path and the request-path are identical."
+ request(this.server)
+ .get('/admin')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should set cookie when cookie-path is prefix and last char is "/" ("/admin/")', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
+ // character of the cookie-path is %x2F ("/")."
+ request(this.server)
+ .get('/admin/')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should set cookie when cookie-path is prefix and next char is "/" ("/admin/users")', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the first
+ // character of the request-path that is not included in the cookie-path is a %x2F ("/") character."
+ request(this.server)
+ .get('/admin/users')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should NOT set cookie when cookie-path is not a prefix ("/administrator")', function (done) {
+ // RFC 6265 5.1.4: None of the path-match conditions are met
+ request(this.server)
+ .get('/administrator')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
+ })
+ })
+
+ describe('when "path" is "/admin/" (trailing slash)', function () {
+ before(function () {
+ this.server = createServer({ cookie: { path: '/admin/' } })
+ })
+
+ it('should set cookie when cookie-path is prefix and last char is "/" ("/admin/x")', function (done) {
+ // RFC 6265 5.1.4: "The cookie-path is a prefix of the request-path, and the last
+ // character of the cookie-path is %x2F ("/")."
+ request(this.server)
+ .get('/admin/x')
+ .expect(shouldSetCookie('connect.sid'))
+ .expect(200, done)
+ })
+
+ it('should NOT set cookie when request-path is not prefixed by cookie-path ("/admin")', function (done) {
+ // RFC 6265 5.1.4: cookie-path "/admin/" is not a prefix of request-path "/admin"
+ request(this.server)
+ .get('/admin')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
+ })
+
+ it('should NOT set cookie when cookie-path is not a prefix ("/administrator")', function (done) {
+ // RFC 6265 5.1.4: None of the path-match conditions are met:
+ // 1. The paths are not identical
+ // 2. "/admin/" is not a prefix of "/administrator"
+ // 3. The prefix condition with next character "/" is not applicable
+ request(this.server)
+ .get('/administrator')
+ .expect(shouldNotHaveHeader('Set-Cookie'))
+ .expect(200, done)
+ })
+ })
+})
+
+
function cookie(res) {
var setCookie = res.headers['set-cookie'];
return (setCookie && setCookie[0]) || undefined;
}
-function createServer(opts, fn) {
- return http.createServer(createRequestListener(opts, fn))
+function createServer (options, respond) {
+ var fn = respond
+ var opts = options
+ var server = http.createServer()
+
+ // setup, options, respond
+ if (typeof arguments[0] === 'function') {
+ opts = arguments[1]
+ fn = arguments[2]
+
+ server.on('request', arguments[0])
+ }
+
+ return server.on('request', createRequestListener(opts, fn))
}
function createRequestListener(opts, fn) {
@@ -2254,9 +2853,18 @@ function end(req, res) {
res.end()
}
-function expires(res) {
- var match = /Expires=([^;]+)/.exec(cookie(res));
- return match ? match[1] : undefined;
+function expires (res) {
+ var header = cookie(res)
+ return header && utils.parseSetCookie(header).expires
+}
+
+function mountAt (path) {
+ return function (req, res) {
+ if (req.url.indexOf(path) === 0) {
+ req.originalUrl = req.url
+ req.url = req.url.slice(path.length)
+ }
+ }
}
function shouldNotHaveHeader(header) {
@@ -2265,87 +2873,118 @@ function shouldNotHaveHeader(header) {
}
}
-function shouldNotSetSecureCookie(name) {
+function shouldNotSetSessionInStore(store) {
+ var _set = store.set
+ var count = 0
+
+ store.set = function set () {
+ count++
+ return _set.apply(this, arguments)
+ }
+
+ return function () {
+ assert.ok(count === 0, 'should not set session in store')
+ }
+}
+
+function shouldSetCookie (name) {
+ return function (res) {
+ var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
+ assert.ok(header, 'should have a cookie header')
+ assert.strictEqual(data.name, name, 'should set cookie ' + name)
+ }
+}
+
+function shouldSetCookieToDifferentSessionId (id) {
+ return function (res) {
+ assert.notStrictEqual(sid(res), id)
+ }
+}
+
+function shouldSetCookieToExpireIn (name, delta) {
return function (res) {
var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
assert.ok(header, 'should have a cookie header')
- assert.equal(header.split('=')[0], name, 'should set cookie ' + name)
- assert.ok(header.toLowerCase().split(/; */).every(function (k) { return k !== 'secure'; }), 'should not set secure cookie')
+ assert.strictEqual(data.name, name, 'should set cookie ' + name)
+ assert.ok(('expires' in data), 'should set cookie with attribute Expires')
+ assert.ok(('date' in res.headers), 'should have a date header')
+ assert.strictEqual((Date.parse(data.expires) - Date.parse(res.headers.date)), delta, 'should set cookie ' + name + ' to expire in ' + delta + ' ms')
}
}
-function shouldSetCookie(name) {
+function shouldSetCookieToValue (name, val) {
return function (res) {
var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
assert.ok(header, 'should have a cookie header')
- assert.equal(header.split('=')[0], name, 'should set cookie ' + name)
+ assert.strictEqual(data.name, name, 'should set cookie ' + name)
+ assert.strictEqual(data.value, val, 'should set cookie ' + name + ' to ' + val)
}
}
-function shouldSetCookieToValue(name, val) {
+function shouldSetCookieWithAttribute (name, attrib) {
return function (res) {
- var header = cookie(res);
+ var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
assert.ok(header, 'should have a cookie header')
- assert.equal(header.split('=')[0], name, 'should set cookie ' + name)
- assert.equal(header.split('=')[1].split(';')[0], val, 'should set cookie ' + name + ' to ' + val)
+ assert.strictEqual(data.name, name, 'should set cookie ' + name)
+ assert.ok((attrib.toLowerCase() in data), 'should set cookie with attribute ' + attrib)
}
}
-function shouldSetSecureCookie(name) {
+function shouldSetCookieWithAttributeAndValue (name, attrib, value) {
return function (res) {
var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
assert.ok(header, 'should have a cookie header')
- assert.equal(header.split('=')[0], name, 'should set cookie ' + name)
- assert.ok(header.toLowerCase().split(/; */).some(function (k) { return k === 'secure'; }), 'should set secure cookie')
+ assert.strictEqual(data.name, name, 'should set cookie ' + name)
+ assert.ok((attrib.toLowerCase() in data), 'should set cookie with attribute ' + attrib)
+ assert.strictEqual(data[attrib.toLowerCase()], value, 'should set cookie with attribute ' + attrib + ' set to ' + value)
}
}
-function sid(res) {
- var match = /^[^=]+=s%3A([^;\.]+)[\.;]/.exec(cookie(res))
- var val = match ? match[1] : undefined
- return val
+function shouldSetCookieWithoutAttribute (name, attrib) {
+ return function (res) {
+ var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
+ assert.ok(header, 'should have a cookie header')
+ assert.strictEqual(data.name, name, 'should set cookie ' + name)
+ assert.ok(!(attrib.toLowerCase() in data), 'should set cookie without attribute ' + attrib)
+ }
}
-function writePatch() {
- var ended = false
- return function addWritePatch(req, res, next) {
- var _end = res.end
- var _write = res.write
+function shouldSetSessionInStore (store, delay) {
+ var _set = store.set
+ var count = 0
- res.end = function end() {
- ended = true
- return _end.apply(this, arguments)
+ store.set = function set () {
+ count++
+
+ if (!delay) {
+ return _set.apply(this, arguments)
}
- res.write = function write() {
- if (ended) {
- throw new Error('write after end')
- }
+ var args = new Array(arguments.length + 1)
- return _write.apply(this, arguments)
+ args[0] = this
+ for (var i = 1; i < args.length; i++) {
+ args[i] = arguments[i - 1]
}
- next()
+ setTimeout(_set.bind.apply(_set, args), delay)
}
-}
-function SyncStore () {
- session.Store.call(this)
- this.sessions = Object.create(null)
+ return function () {
+ assert.ok(count === 1, 'should set session in store')
+ }
}
-util.inherits(SyncStore, session.Store)
-
-SyncStore.prototype.destroy = function destroy(sid, callback) {
- delete this.sessions[sid];
- callback();
-};
-
-SyncStore.prototype.get = function get(sid, callback) {
- callback(null, JSON.parse(this.sessions[sid]));
-};
-
-SyncStore.prototype.set = function set(sid, sess, callback) {
- this.sessions[sid] = JSON.stringify(sess);
- callback();
-};
+function sid (res) {
+ var header = cookie(res)
+ var data = header && utils.parseSetCookie(header)
+ var value = data && unescape(data.value)
+ var sid = value && value.substring(2, value.indexOf('.'))
+ return sid || undefined
+}
diff --git a/test/support/env.js b/test/support/env.js
new file mode 100644
index 00000000..fba1bd89
--- /dev/null
+++ b/test/support/env.js
@@ -0,0 +1,2 @@
+
+process.env.NO_DEPRECATION = 'express-session';
diff --git a/test/support/gencert.sh b/test/support/gencert.sh
new file mode 100755
index 00000000..cfdda070
--- /dev/null
+++ b/test/support/gencert.sh
@@ -0,0 +1,5 @@
+#! /bin/sh
+set -ex
+
+openssl req -x509 -nodes -newkey rsa:2048 -keyout ./test/fixtures/server.key -out ./test/fixtures/server.crt -days 3650 \
+-subj "/C=US/ST=Illinois/L=Chicago/O=node-express-session/CN=express-session.local"
diff --git a/test/support/smart-store.js b/test/support/smart-store.js
new file mode 100644
index 00000000..8b224fdd
--- /dev/null
+++ b/test/support/smart-store.js
@@ -0,0 +1,54 @@
+'use strict'
+
+var session = require('../../')
+var util = require('util')
+
+/* istanbul ignore next */
+var defer = typeof setImmediate === 'function'
+ ? setImmediate
+ : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
+
+module.exports = SmartStore
+
+function SmartStore () {
+ session.Store.call(this)
+ this.sessions = Object.create(null)
+}
+
+util.inherits(SmartStore, session.Store)
+
+SmartStore.prototype.destroy = function destroy (sid, callback) {
+ delete this.sessions[sid]
+ defer(callback, null)
+}
+
+SmartStore.prototype.get = function get (sid, callback) {
+ var sess = this.sessions[sid]
+
+ if (!sess) {
+ return
+ }
+
+ // parse
+ sess = JSON.parse(sess)
+
+ if (sess.cookie) {
+ // expand expires into Date object
+ sess.cookie.expires = typeof sess.cookie.expires === 'string'
+ ? new Date(sess.cookie.expires)
+ : sess.cookie.expires
+
+ // destroy expired session
+ if (sess.cookie.expires && sess.cookie.expires <= Date.now()) {
+ delete this.sessions[sid]
+ sess = null
+ }
+ }
+
+ defer(callback, null, sess)
+}
+
+SmartStore.prototype.set = function set (sid, sess, callback) {
+ this.sessions[sid] = JSON.stringify(sess)
+ defer(callback, null)
+}
diff --git a/test/support/sync-store.js b/test/support/sync-store.js
new file mode 100644
index 00000000..b45dc9a8
--- /dev/null
+++ b/test/support/sync-store.js
@@ -0,0 +1,27 @@
+'use strict'
+
+var session = require('../../')
+var util = require('util')
+
+module.exports = SyncStore
+
+function SyncStore () {
+ session.Store.call(this)
+ this.sessions = Object.create(null)
+}
+
+util.inherits(SyncStore, session.Store)
+
+SyncStore.prototype.destroy = function destroy (sid, callback) {
+ delete this.sessions[sid]
+ callback()
+}
+
+SyncStore.prototype.get = function get (sid, callback) {
+ callback(null, JSON.parse(this.sessions[sid]))
+}
+
+SyncStore.prototype.set = function set (sid, sess, callback) {
+ this.sessions[sid] = JSON.stringify(sess)
+ callback()
+}
diff --git a/test/support/utils.js b/test/support/utils.js
new file mode 100644
index 00000000..0f13ba6a
--- /dev/null
+++ b/test/support/utils.js
@@ -0,0 +1,42 @@
+'use strict'
+
+module.exports.parseSetCookie = parseSetCookie
+module.exports.writePatch = writePatch
+
+function parseSetCookie (header) {
+ var match
+ var pairs = []
+ var pattern = /\s*([^=;]+)(?:=([^;]*);?|;|$)/g
+
+ while ((match = pattern.exec(header))) {
+ pairs.push({ name: match[1], value: match[2] })
+ }
+
+ var cookie = pairs.shift()
+
+ for (var i = 0; i < pairs.length; i++) {
+ match = pairs[i]
+ cookie[match.name.toLowerCase()] = (match.value || true)
+ }
+
+ return cookie
+}
+
+function writePatch (res) {
+ var _end = res.end
+ var _write = res.write
+ var ended = false
+
+ res.end = function end () {
+ ended = true
+ return _end.apply(this, arguments)
+ }
+
+ res.write = function write () {
+ if (ended) {
+ throw new Error('write after end')
+ }
+
+ return _write.apply(this, arguments)
+ }
+}