@fastify/cookie 7.3.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,4 +14,5 @@ jobs:
14
14
  test:
15
15
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
16
16
  with:
17
+ license-check: true
17
18
  lint: true
package/README.md CHANGED
@@ -33,7 +33,8 @@ const fastify = require('fastify')()
33
33
 
34
34
  fastify.register(require('@fastify/cookie'), {
35
35
  secret: "my-secret", // for cookies signature
36
- parseOptions: {} // options for parsing cookies
36
+ hook: 'onRequest', // set to false to disable cookie autoparsing or set autoparsing on any of the following hooks: 'onRequest', 'preParsing', 'preHandler', 'preValidation'. default: 'onRequest'
37
+ parseOptions: {} // options for parsing cookies
37
38
  })
38
39
 
39
40
  fastify.get('/', (req, reply) => {
@@ -192,9 +193,9 @@ fastify.register(require('@fastify/cookie'), {
192
193
 
193
194
  ### Manual cookie unsigning
194
195
 
195
- The method `unsignCookie(value)` is added to the `fastify` instance and the `reply` object
196
- via the Fastify `decorate` & `decorateReply` APIs. Using it on a signed cookie will call the
197
- the provided signer's (or the default signer if no custom implementation is provided) `unsign` method on the cookie.
196
+ The method `unsignCookie(value)` is added to the `fastify` instance, to the `request` and the `reply` object
197
+ via the Fastify `decorate`, `decorateRequest` and `decorateReply` APIs, if a secret was provided as option.
198
+ Using it on a signed cookie will call the the provided signer's (or the default signer if no custom implementation is provided) `unsign` method on the cookie.
198
199
 
199
200
  **Example:**
200
201
 
@@ -236,12 +237,12 @@ test('Request requires signed cookie', async () => {
236
237
 
237
238
  ### Manual signing/unsigning with low level utilities
238
239
 
239
- with signerFactory
240
+ with Signer
240
241
 
241
242
  ```js
242
- const { fastifyCookie } = require('@fastify/cookie');
243
+ const { Signer } = require('@fastify/cookie');
243
244
 
244
- const signer = fastifyCookie.signerFactory('secret');
245
+ const signer = new Signer('secret');
245
246
  const signedValue = signer.sign('test');
246
247
  const {valid, renew, value } = signer.unsign(signedValue);
247
248
  ```
@@ -0,0 +1,18 @@
1
+ const Benchmark = require('benchmark')
2
+ const Signer = require('../signer')
3
+ const suite = new Benchmark.Suite()
4
+
5
+ const signer = Signer(['abcd', 'asdf', 'vadfa'])
6
+ const signedValue = signer.sign('test', 'vadfa')
7
+
8
+ suite
9
+ .add('sign', function () {
10
+ signer.sign('test')
11
+ })
12
+ .add('unsign', function () {
13
+ signer.unsign(signedValue)
14
+ })
15
+ .on('cycle', function (event) {
16
+ console.log(String(event.target))
17
+ })
18
+ .run({ delay: 0 })
@@ -0,0 +1,17 @@
1
+ const Benchmark = require('benchmark')
2
+ const Signer = require('../signer')
3
+ const suite = new Benchmark.Suite()
4
+
5
+ const signer = Signer('abcd')
6
+ const signedValue = signer.sign('test')
7
+ suite
8
+ .add('sign', function () {
9
+ signer.sign('test')
10
+ })
11
+ .add('unsign', function () {
12
+ signer.unsign(signedValue)
13
+ })
14
+ .on('cycle', function (event) {
15
+ console.log(String(event.target))
16
+ })
17
+ .run({ delay: 0 })
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@fastify/cookie",
3
- "version": "7.3.1",
3
+ "version": "8.1.0",
4
4
  "description": "Plugin for fastify to add support for cookies",
5
5
  "main": "plugin.js",
6
- "types": "plugin.d.ts",
6
+ "types": "types/plugin.d.ts",
7
7
  "scripts": {
8
8
  "lint": "standard | snazzy",
9
9
  "lint:ci": "standard",
@@ -39,19 +39,18 @@
39
39
  },
40
40
  "homepage": "https://github.com/fastify/fastify-cookie#readme",
41
41
  "devDependencies": {
42
+ "@fastify/pre-commit": "^2.0.2",
42
43
  "@types/node": "^18.0.0",
43
- "fastify": "^4.0.0-rc.2",
44
- "pre-commit": "^1.2.2",
44
+ "benchmark": "^2.1.4",
45
+ "fastify": "^4.0.0",
45
46
  "sinon": "^14.0.0",
46
47
  "snazzy": "^9.0.0",
47
48
  "standard": "^17.0.0",
48
49
  "tap": "^16.0.0",
49
- "tsd": "^0.22.0",
50
- "typescript": "^4.5.5"
50
+ "tsd": "^0.22.0"
51
51
  },
52
52
  "dependencies": {
53
53
  "cookie": "^0.5.0",
54
- "cookie-signature": "^1.1.0",
55
54
  "fastify-plugin": "^4.0.0"
56
55
  },
57
56
  "tsd": {
package/plugin.js CHANGED
@@ -1,10 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { sign, unsign } = require('cookie-signature')
4
3
  const fp = require('fastify-plugin')
5
4
  const cookie = require('cookie')
6
5
 
7
- const signerFactory = require('./signer')
6
+ const { Signer, sign, unsign } = require('./signer')
8
7
 
9
8
  function fastifyCookieSetCookie (reply, name, value, options, signer) {
10
9
  const opts = Object.assign({}, options)
@@ -51,34 +50,70 @@ function fastifyCookieClearCookie (reply, name, options) {
51
50
  return fastifyCookieSetCookie(reply, name, '', opts)
52
51
  }
53
52
 
54
- function onReqHandlerWrapper (fastify) {
55
- return function fastifyCookieOnReqHandler (fastifyReq, fastifyRes, done) {
56
- fastifyReq.cookies = {} // New container per request. Issue #53
57
- const cookieHeader = fastifyReq.raw.headers.cookie
58
- if (cookieHeader) {
59
- fastifyReq.cookies = fastify.parseCookie(cookieHeader)
53
+ function onReqHandlerWrapper (fastify, hook) {
54
+ return hook === 'preParsing'
55
+ ? function fastifyCookieHandler (fastifyReq, fastifyRes, payload, done) {
56
+ fastifyReq.cookies = {} // New container per request. Issue #53
57
+ const cookieHeader = fastifyReq.raw.headers.cookie
58
+ if (cookieHeader) {
59
+ fastifyReq.cookies = fastify.parseCookie(cookieHeader)
60
+ }
61
+ done()
60
62
  }
61
- done()
63
+ : function fastifyCookieHandler (fastifyReq, fastifyRes, done) {
64
+ fastifyReq.cookies = {} // New container per request. Issue #53
65
+ const cookieHeader = fastifyReq.raw.headers.cookie
66
+ if (cookieHeader) {
67
+ fastifyReq.cookies = fastify.parseCookie(cookieHeader)
68
+ }
69
+ done()
70
+ }
71
+ }
72
+
73
+ function getHook (hook = 'onRequest') {
74
+ const hooks = {
75
+ onRequest: 'onRequest',
76
+ preParsing: 'preParsing',
77
+ preValidation: 'preValidation',
78
+ preHandler: 'preHandler',
79
+ [false]: false
62
80
  }
81
+
82
+ return hooks[hook]
63
83
  }
64
84
 
65
85
  function plugin (fastify, options, next) {
66
- const secret = options.secret || ''
86
+ const secret = options.secret
87
+ const hook = getHook(options.hook)
88
+ if (hook === undefined) {
89
+ return next(new Error('@fastify/cookie: Invalid value provided for the hook-option. You can set the hook-option only to false, \'onRequest\' , \'preParsing\' , \'preValidation\' or \'preHandler\''))
90
+ }
67
91
  const enableRotation = Array.isArray(secret)
68
- const signer = typeof secret === 'string' || enableRotation ? signerFactory(secret) : secret
92
+ const algorithm = options.algorithm || 'sha256'
93
+ const signer = typeof secret === 'string' || enableRotation ? new Signer(secret, algorithm) : secret
69
94
 
70
95
  fastify.decorate('parseCookie', parseCookie)
71
- fastify.decorate('signCookie', signCookie)
72
- fastify.decorate('unsignCookie', unsignCookie)
96
+
97
+ if (typeof secret !== 'undefined') {
98
+ fastify.decorate('signCookie', signCookie)
99
+ fastify.decorate('unsignCookie', unsignCookie)
100
+
101
+ fastify.decorateRequest('signCookie', signCookie)
102
+ fastify.decorateRequest('unsignCookie', unsignCookie)
103
+
104
+ fastify.decorateReply('signCookie', signCookie)
105
+ fastify.decorateReply('unsignCookie', unsignCookie)
106
+ }
73
107
 
74
108
  fastify.decorateRequest('cookies', null)
75
- fastify.decorateRequest('unsignCookie', unsignCookie)
76
- fastify.decorateReply('setCookie', setCookie)
77
109
  fastify.decorateReply('cookie', setCookie)
110
+
111
+ fastify.decorateReply('setCookie', setCookie)
78
112
  fastify.decorateReply('clearCookie', clearCookie)
79
- fastify.decorateReply('unsignCookie', unsignCookie)
80
113
 
81
- fastify.addHook('onRequest', onReqHandlerWrapper(fastify))
114
+ if (hook) {
115
+ fastify.addHook(hook, onReqHandlerWrapper(fastify, hook))
116
+ }
82
117
 
83
118
  next()
84
119
 
@@ -127,14 +162,17 @@ const fastifyCookie = fp(plugin, {
127
162
  * - `import { fastifyCookie } from 'fastify-cookie'`
128
163
  * - `import fastifyCookie from 'fastify-cookie'`
129
164
  */
165
+ fastifyCookie.signerFactory = Signer
130
166
  fastifyCookie.fastifyCookie = fastifyCookie
131
167
  fastifyCookie.default = fastifyCookie
132
168
  module.exports = fastifyCookie
133
169
 
134
- fastifyCookie.fastifyCookie.signerFactory = signerFactory
170
+ fastifyCookie.fastifyCookie.signerFactory = Signer
171
+ fastifyCookie.fastifyCookie.Signer = Signer
135
172
  fastifyCookie.fastifyCookie.sign = sign
136
173
  fastifyCookie.fastifyCookie.unsign = unsign
137
174
 
138
- module.exports.signerFactory = signerFactory
175
+ module.exports.signerFactory = Signer
176
+ module.exports.Signer = Signer
139
177
  module.exports.sign = sign
140
178
  module.exports.unsign = unsign
package/signer.js CHANGED
@@ -1,32 +1,116 @@
1
1
  'use strict'
2
2
 
3
- const cookieSignature = require('cookie-signature')
3
+ // Inspired by node-cookie-signature
4
+ // https://github.com/tj/node-cookie-signature
5
+ //
6
+ // The MIT License
7
+ // Copyright (c) 2012–2022 LearnBoost <tj@learnboost.com> and other contributors;
4
8
 
5
- module.exports = function (secret) {
6
- const secrets = Array.isArray(secret) ? secret : [secret]
7
- const [signingKey] = secrets
9
+ const crypto = require('crypto')
8
10
 
9
- return {
10
- sign (value) {
11
- return cookieSignature.sign(value, signingKey)
12
- },
13
- unsign (signedValue) {
14
- let valid = false
15
- let renew = false
16
- let value = null
17
-
18
- for (const key of secrets) {
19
- const result = cookieSignature.unsign(signedValue, key)
20
-
21
- if (result !== false) {
22
- valid = true
23
- renew = key !== signingKey
24
- value = result
25
- break
26
- }
27
- }
11
+ const base64PaddingRE = /=/g
12
+
13
+ function Signer (secrets, algorithm = 'sha256') {
14
+ if (!(this instanceof Signer)) {
15
+ return new Signer(secrets, algorithm)
16
+ }
28
17
 
29
- return { valid, renew, value }
18
+ this.secrets = Array.isArray(secrets) ? secrets : [secrets]
19
+ this.signingKey = this.secrets[0]
20
+ this.algorithm = algorithm
21
+
22
+ validateSecrets(this.secrets)
23
+ validateAlgorithm(this.algorithm)
24
+ }
25
+
26
+ function validateSecrets (secrets) {
27
+ for (const secret of secrets) {
28
+ if (typeof secret !== 'string') {
29
+ throw new TypeError('Secret key must be a string.')
30
30
  }
31
31
  }
32
32
  }
33
+
34
+ function validateAlgorithm (algorithm) {
35
+ // validate that the algorithm is supported by the node runtime
36
+ try {
37
+ crypto.createHmac(algorithm, crypto.randomBytes(16))
38
+ } catch (e) {
39
+ throw new TypeError(`Algorithm ${algorithm} not supported.`)
40
+ }
41
+ }
42
+
43
+ function _sign (value, secret, algorithm) {
44
+ if (typeof value !== 'string') {
45
+ throw new TypeError('Cookie value must be provided as a string.')
46
+ }
47
+ return value + '.' + crypto
48
+ .createHmac(algorithm, secret)
49
+ .update(value)
50
+ .digest('base64')
51
+ // remove base64 padding (=) as it has special meaning in cookies
52
+ .replace(base64PaddingRE, '')
53
+ }
54
+
55
+ function _unsign (signedValue, secrets, algorithm) {
56
+ if (typeof signedValue !== 'string') {
57
+ throw new TypeError('Signed cookie string must be provided.')
58
+ }
59
+ const value = signedValue.slice(0, signedValue.lastIndexOf('.'))
60
+ const actual = Buffer.from(signedValue.slice(signedValue.lastIndexOf('.') + 1))
61
+
62
+ for (const secret of secrets) {
63
+ const expected = Buffer.from(crypto
64
+ .createHmac(algorithm, secret)
65
+ .update(value)
66
+ .digest('base64')
67
+ // remove base64 padding (=) as it has special meaning in cookies
68
+ .replace(base64PaddingRE, ''))
69
+ if (
70
+ expected.length === actual.length &&
71
+ crypto.timingSafeEqual(expected, actual)
72
+ ) {
73
+ return {
74
+ valid: true,
75
+ renew: secret !== secrets[0],
76
+ value
77
+ }
78
+ }
79
+ }
80
+
81
+ return {
82
+ valid: false,
83
+ renew: false,
84
+ value: null
85
+ }
86
+ }
87
+
88
+ Signer.prototype.sign = function (value) {
89
+ return _sign(value, this.signingKey, this.algorithm)
90
+ }
91
+
92
+ Signer.prototype.unsign = function (signedValue) {
93
+ return _unsign(signedValue, this.secrets, this.algorithm)
94
+ }
95
+
96
+ function sign (value, secret, algorithm = 'sha256') {
97
+ const secrets = Array.isArray(secret) ? secret : [secret]
98
+
99
+ validateSecrets(secrets)
100
+
101
+ return _sign(value, secrets[0], algorithm)
102
+ }
103
+
104
+ function unsign (signedValue, secret, algorithm = 'sha256') {
105
+ const secrets = Array.isArray(secret) ? secret : [secret]
106
+
107
+ validateSecrets(secrets)
108
+
109
+ return _unsign(signedValue, secrets, algorithm)
110
+ }
111
+
112
+ module.exports = Signer
113
+ module.exports.signerFactory = Signer
114
+ module.exports.Signer = Signer
115
+ module.exports.sign = sign
116
+ module.exports.unsign = unsign
@@ -4,7 +4,7 @@ const tap = require('tap')
4
4
  const test = tap.test
5
5
  const Fastify = require('fastify')
6
6
  const sinon = require('sinon')
7
- const cookieSignature = require('cookie-signature')
7
+ const { sign, unsign } = require('../signer')
8
8
  const plugin = require('../')
9
9
 
10
10
  test('cookies get set correctly', (t) => {
@@ -151,7 +151,7 @@ test('share options for setCookie and clearCookie', (t) => {
151
151
  const cookies = res.cookies
152
152
  t.equal(cookies.length, 2)
153
153
  t.equal(cookies[0].name, 'foo')
154
- t.equal(cookies[0].value, cookieSignature.sign('foo', secret))
154
+ t.equal(cookies[0].value, sign('foo', secret))
155
155
  t.equal(cookies[0].maxAge, 36000)
156
156
 
157
157
  t.equal(cookies[1].name, 'foo')
@@ -190,7 +190,7 @@ test('expires should not be overridden in clearCookie', (t) => {
190
190
  const cookies = res.cookies
191
191
  t.equal(cookies.length, 2)
192
192
  t.equal(cookies[0].name, 'foo')
193
- t.equal(cookies[0].value, cookieSignature.sign('foo', secret))
193
+ t.equal(cookies[0].value, sign('foo', secret))
194
194
  const expires = new Date(cookies[0].expires)
195
195
  t.ok(expires < new Date(Date.now() + 5000))
196
196
 
@@ -378,7 +378,7 @@ test('cookies signature', (t) => {
378
378
  const cookies = res.cookies
379
379
  t.equal(cookies.length, 1)
380
380
  t.equal(cookies[0].name, 'foo')
381
- t.equal(cookieSignature.unsign(cookies[0].value, secret), 'foo')
381
+ t.same(unsign(cookies[0].value, secret), { valid: true, renew: false, value: 'foo' })
382
382
  })
383
383
  })
384
384
 
@@ -406,7 +406,7 @@ test('cookies signature', (t) => {
406
406
  const cookies = res.cookies
407
407
  t.equal(cookies.length, 1)
408
408
  t.equal(cookies[0].name, 'foo')
409
- t.equal(cookieSignature.unsign(cookies[0].value, secret1), 'cookieVal') // decode using first key
409
+ t.same(unsign(cookies[0].value, secret1), { valid: true, renew: false, value: 'cookieVal' }) // decode using first key
410
410
  })
411
411
  })
412
412
 
@@ -427,7 +427,7 @@ test('cookies signature', (t) => {
427
427
  method: 'GET',
428
428
  url: '/test1',
429
429
  headers: {
430
- cookie: `foo=${cookieSignature.sign('foo', secret)}`
430
+ cookie: `foo=${sign('foo', secret)}`
431
431
  }
432
432
  }, (err, res) => {
433
433
  t.error(err)
@@ -452,7 +452,7 @@ test('cookies signature', (t) => {
452
452
  method: 'GET',
453
453
  url: '/test1',
454
454
  headers: {
455
- cookie: `foo=${cookieSignature.sign('foo', secret)}`
455
+ cookie: `foo=${sign('foo', secret)}`
456
456
  }
457
457
  }, (err, res) => {
458
458
  t.error(err)
@@ -477,7 +477,7 @@ test('cookies signature', (t) => {
477
477
  method: 'GET',
478
478
  url: '/test1',
479
479
  headers: {
480
- cookie: `foo=${cookieSignature.sign('foo', secret)}`
480
+ cookie: `foo=${sign('foo', secret)}`
481
481
  }
482
482
  }, (err, res) => {
483
483
  t.error(err)
@@ -503,7 +503,7 @@ test('cookies signature', (t) => {
503
503
  method: 'GET',
504
504
  url: '/test1',
505
505
  headers: {
506
- cookie: `foo=${cookieSignature.sign('foo', secret2)}`
506
+ cookie: `foo=${sign('foo', secret2)}`
507
507
  }
508
508
  }, (err, res) => {
509
509
  t.error(err)
@@ -529,7 +529,7 @@ test('cookies signature', (t) => {
529
529
  method: 'GET',
530
530
  url: '/test1',
531
531
  headers: {
532
- cookie: `foo=${cookieSignature.sign('foo', secret2)}`
532
+ cookie: `foo=${sign('foo', secret2)}`
533
533
  }
534
534
  }, (err, res) => {
535
535
  t.error(err)
@@ -555,7 +555,7 @@ test('cookies signature', (t) => {
555
555
  method: 'GET',
556
556
  url: '/test1',
557
557
  headers: {
558
- cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}`
558
+ cookie: `foo=${sign('foo', 'invalid-secret')}`
559
559
  }
560
560
  }, (err, res) => {
561
561
  t.error(err)
@@ -581,7 +581,7 @@ test('cookies signature', (t) => {
581
581
  method: 'GET',
582
582
  url: '/test1',
583
583
  headers: {
584
- cookie: `foo=${cookieSignature.sign('foo', 'invalid-secret')}`
584
+ cookie: `foo=${sign('foo', 'invalid-secret')}`
585
585
  }
586
586
  }, (err, res) => {
587
587
  t.error(err)
@@ -814,3 +814,131 @@ test('handle secure:auto of cookieOptions', async (t) => {
814
814
  t.same(cookies2[0].secure, null)
815
815
  t.equal(cookies2[0].path, '/')
816
816
  })
817
+
818
+ test('should not decorate fastify, request and reply if no secret was provided', async (t) => {
819
+ t.plan(8)
820
+ const fastify = Fastify()
821
+
822
+ await fastify.register(plugin)
823
+
824
+ t.notOk(fastify.signCookie)
825
+ t.notOk(fastify.unsignCookie)
826
+
827
+ fastify.get('/testDecorators', (req, reply) => {
828
+ t.notOk(req.signCookie)
829
+ t.notOk(reply.signCookie)
830
+ t.notOk(req.unsignCookie)
831
+ t.notOk(reply.unsignCookie)
832
+
833
+ reply.send({
834
+ unsigned: req.unsignCookie(req.cookies.foo)
835
+ })
836
+ })
837
+
838
+ const res = await fastify.inject({
839
+ method: 'GET',
840
+ url: '/testDecorators'
841
+ })
842
+
843
+ t.equal(res.statusCode, 500)
844
+ t.same(JSON.parse(res.body), {
845
+ statusCode: 500,
846
+ error: 'Internal Server Error',
847
+ message: 'req.unsignCookie is not a function'
848
+ })
849
+ })
850
+
851
+ test('dont add auto cookie parsing to onRequest-hook if hook-option is set to false', (t) => {
852
+ t.plan(6)
853
+ const fastify = Fastify()
854
+ fastify.register(plugin, { hook: false })
855
+
856
+ for (const hook of ['preValidation', 'preHandler', 'preParsing']) {
857
+ fastify.addHook(hook, async (req) => {
858
+ t.equal(req.cookies, null)
859
+ })
860
+ }
861
+
862
+ fastify.get('/disable', (req, reply) => {
863
+ t.equal(req.cookies, null)
864
+ reply.send()
865
+ })
866
+
867
+ fastify.inject({
868
+ method: 'GET',
869
+ url: '/disable',
870
+ headers: {
871
+ cookie: 'bar=bar'
872
+ }
873
+ }, (err, res) => {
874
+ t.error(err)
875
+ t.equal(res.statusCode, 200)
876
+ })
877
+ })
878
+
879
+ test('result in an error if hook-option is set to an invalid value', (t) => {
880
+ t.plan(1)
881
+ const fastify = Fastify()
882
+
883
+ t.rejects(
884
+ () => fastify.register(plugin, { hook: true }),
885
+ new Error("@fastify/cookie: Invalid value provided for the hook-option. You can set the hook-option only to false, 'onRequest' , 'preParsing' , 'preValidation' or 'preHandler'")
886
+ )
887
+ })
888
+
889
+ test('correct working plugin if hook-option to preParsing', (t) => {
890
+ t.plan(5)
891
+ const fastify = Fastify()
892
+ fastify.register(plugin, { hook: 'preParsing' })
893
+
894
+ fastify.addHook('onRequest', async (req) => {
895
+ t.equal(req.cookies, null)
896
+ })
897
+
898
+ fastify.addHook('preValidation', async (req) => {
899
+ t.equal(req.cookies.bar, 'bar')
900
+ })
901
+
902
+ fastify.get('/preparsing', (req, reply) => {
903
+ t.equal(req.cookies.bar, 'bar')
904
+ reply.send()
905
+ })
906
+
907
+ fastify.inject({
908
+ method: 'GET',
909
+ url: '/preparsing',
910
+ headers: {
911
+ cookie: 'bar=bar'
912
+ }
913
+ }, (err, res) => {
914
+ t.error(err)
915
+ t.equal(res.statusCode, 200)
916
+ })
917
+ })
918
+
919
+ test('if cookies are not set, then the handler creates an empty req.cookies object', (t) => {
920
+ t.plan(5)
921
+ const fastify = Fastify()
922
+ fastify.register(plugin, { hook: 'preParsing' })
923
+
924
+ fastify.addHook('onRequest', async (req) => {
925
+ t.equal(req.cookies, null)
926
+ })
927
+
928
+ fastify.addHook('preValidation', async (req) => {
929
+ t.ok(req.cookies)
930
+ })
931
+
932
+ fastify.get('/preparsing', (req, reply) => {
933
+ t.ok(req.cookies)
934
+ reply.send()
935
+ })
936
+
937
+ fastify.inject({
938
+ method: 'GET',
939
+ url: '/preparsing'
940
+ }, (err, res) => {
941
+ t.error(err)
942
+ t.equal(res.statusCode, 200)
943
+ })
944
+ })
@@ -2,33 +2,67 @@
2
2
 
3
3
  const { test } = require('tap')
4
4
  const sinon = require('sinon')
5
- const cookieSignature = require('cookie-signature')
6
- const signerFactory = require('../signer')
5
+ const crypto = require('crypto')
6
+ const { Signer, sign, unsign } = require('../signer')
7
7
 
8
- test('default', (t) => {
9
- t.plan(2)
8
+ test('default', t => {
9
+ t.plan(5)
10
10
 
11
11
  const secret = 'my-secret'
12
- const signer = signerFactory(secret)
12
+ const signer = Signer(secret)
13
13
 
14
- t.test('signer.sign', (t) => {
14
+ t.test('signer.sign should throw if there is no value provided', (t) => {
15
15
  t.plan(1)
16
16
 
17
+ t.throws(() => signer.sign(undefined), 'Cookie value must be provided as a string.')
18
+ })
19
+
20
+ t.test('signer.sign', (t) => {
21
+ t.plan(2)
22
+
17
23
  const input = 'some-value'
18
24
  const result = signer.sign(input)
19
25
 
20
- t.equal(result, cookieSignature.sign(input, secret))
26
+ t.equal(result, sign(input, secret))
27
+ t.throws(() => sign(undefined), 'Cookie value must be provided as a string.')
21
28
  })
22
29
 
23
- t.test('signer.unsign', (t) => {
30
+ t.test('sign', (t) => {
24
31
  t.plan(3)
25
32
 
26
- const input = cookieSignature.sign('some-value', secret)
33
+ const input = 'some-value'
34
+ const result = signer.sign(input)
35
+
36
+ t.equal(result, sign(input, secret))
37
+ t.equal(result, sign(input, [secret]))
38
+
39
+ t.throws(() => sign(undefined), 'Cookie value must be provided as a string.')
40
+ })
41
+
42
+ t.test('signer.unsign', (t) => {
43
+ t.plan(4)
44
+
45
+ const input = signer.sign('some-value', secret)
27
46
  const result = signer.unsign(input)
28
47
 
29
48
  t.equal(result.valid, true)
30
49
  t.equal(result.renew, false)
31
50
  t.equal(result.value, 'some-value')
51
+ t.throws(() => signer.unsign(undefined), 'Signed cookie string must be provided.')
52
+ })
53
+
54
+ t.test('unsign', (t) => {
55
+ t.plan(6)
56
+
57
+ const input = sign('some-value', secret)
58
+ const result = unsign(input, secret)
59
+
60
+ t.equal(result.valid, true)
61
+ t.equal(result.renew, false)
62
+ t.equal(result.value, 'some-value')
63
+ t.same(result, unsign(input, [secret]))
64
+ t.throws(() => unsign(undefined), 'Secret key must be a string.')
65
+ t.throws(() => unsign(undefined, secret), 'Signed cookie string must be provided.')
32
66
  })
33
67
  })
34
68
 
@@ -37,11 +71,11 @@ test('key rotation', (t) => {
37
71
  const secret1 = 'my-secret-1'
38
72
  const secret2 = 'my-secret-2'
39
73
  const secret3 = 'my-secret-3'
40
- const signer = signerFactory([secret1, secret2, secret3])
41
- const unsignSpy = sinon.spy(cookieSignature, 'unsign')
74
+ const signer = Signer([secret1, secret2, secret3])
75
+ const signSpy = sinon.spy(crypto, 'createHmac')
42
76
 
43
77
  t.beforeEach(() => {
44
- unsignSpy.resetHistory()
78
+ signSpy.resetHistory()
45
79
  })
46
80
 
47
81
  t.test('signer.sign always signs using first key', (t) => {
@@ -50,30 +84,51 @@ test('key rotation', (t) => {
50
84
  const input = 'some-value'
51
85
  const result = signer.sign(input)
52
86
 
53
- t.equal(result, cookieSignature.sign(input, secret1))
87
+ t.equal(result, sign(input, secret1))
54
88
  })
55
89
 
56
90
  t.test('signer.unsign tries to decode using all keys till it finds', (t) => {
57
91
  t.plan(4)
58
92
 
59
- const input = cookieSignature.sign('some-value', secret2)
93
+ const input = sign('some-value', secret2)
94
+ signSpy.resetHistory()
60
95
  const result = signer.unsign(input)
61
96
 
62
97
  t.equal(result.valid, true)
63
98
  t.equal(result.renew, true)
64
99
  t.equal(result.value, 'some-value')
65
- t.equal(unsignSpy.callCount, 2) // should have returned early when the right key was found
100
+ t.equal(signSpy.callCount, 2) // should have returned early when the right key was found
66
101
  })
67
102
 
68
103
  t.test('signer.unsign failure response', (t) => {
69
104
  t.plan(4)
70
105
 
71
- const input = cookieSignature.sign('some-value', 'invalid-secret')
106
+ const input = sign('some-value', 'invalid-secret')
107
+ signSpy.resetHistory()
72
108
  const result = signer.unsign(input)
73
109
 
74
110
  t.equal(result.valid, false)
75
111
  t.equal(result.renew, false)
76
112
  t.equal(result.value, null)
77
- t.equal(unsignSpy.callCount, 3) // should have tried all 3
113
+ t.equal(signSpy.callCount, 3) // should have tried all 3
114
+ })
115
+ })
116
+
117
+ test('Signer', t => {
118
+ t.plan(2)
119
+
120
+ t.test('Signer needs a string as secret', (t) => {
121
+ t.plan(4)
122
+ t.throws(() => Signer(1), 'Secret key must be a string.')
123
+ t.throws(() => Signer(undefined), 'Secret key must be a string.')
124
+ t.doesNotThrow(() => Signer('secret'))
125
+ t.doesNotThrow(() => Signer(['secret']))
126
+ })
127
+
128
+ t.test('Signer handles algorithm properly', (t) => {
129
+ t.plan(3)
130
+ t.throws(() => Signer('secret', 'invalid'), 'Algorithm invalid not supported.')
131
+ t.doesNotThrow(() => Signer('secret', 'sha512'))
132
+ t.doesNotThrow(() => Signer('secret', 'sha256'))
78
133
  })
79
134
  })
@@ -3,16 +3,7 @@
3
3
  import { FastifyPluginCallback } from "fastify";
4
4
 
5
5
  declare module "fastify" {
6
- interface FastifyInstance {
7
- /**
8
- * Unsigns the specified cookie using the secret provided.
9
- * @param value Cookie value
10
- */
11
- unsignCookie(value: string): {
12
- valid: boolean;
13
- renew: boolean;
14
- value: string | null;
15
- };
6
+ interface FastifyInstance extends SignerMethods {
16
7
  /**
17
8
  * Manual cookie parsing method
18
9
  * @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
@@ -21,29 +12,34 @@ declare module "fastify" {
21
12
  parseCookie(cookieHeader: string): {
22
13
  [key: string]: string;
23
14
  };
15
+ }
16
+
17
+ interface FastifyRequest extends SignerMethods {
24
18
  /**
25
- * Manual cookie signing method
26
- * @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
27
- * @param value cookie value
19
+ * Request cookies
28
20
  */
29
- signCookie(value: string): string;
21
+ cookies: { [cookieName: string]: string | undefined };
30
22
  }
31
23
 
32
- interface FastifyRequest {
24
+ interface FastifyReply extends SignerMethods {
33
25
  /**
34
26
  * Request cookies
35
27
  */
36
28
  cookies: { [cookieName: string]: string | undefined };
29
+ }
37
30
 
31
+ interface SignerMethods {
38
32
  /**
39
- * Unsigns the specified cookie using the secret provided.
33
+ * Signs the specified cookie using the secret/signer provided.
34
+ * @param value cookie value
35
+ */
36
+ signCookie(value: string): string;
37
+
38
+ /**
39
+ * Unsigns the specified cookie using the secret/signer provided.
40
40
  * @param value Cookie value
41
41
  */
42
- unsignCookie(value: string): {
43
- valid: boolean;
44
- renew: boolean;
45
- value: string | null;
46
- };
42
+ unsignCookie(value: string): fastifyCookie.UnsignResult;
47
43
  }
48
44
 
49
45
  export type setCookieWrapper = (
@@ -89,11 +85,7 @@ declare module "fastify" {
89
85
  * Unsigns the specified cookie using the secret provided.
90
86
  * @param value Cookie value
91
87
  */
92
- unsignCookie(value: string): {
93
- valid: boolean;
94
- renew: boolean;
95
- value: string | null;
96
- };
88
+ unsignCookie(value: string): fastifyCookie.UnsignResult;
97
89
  }
98
90
  }
99
91
 
@@ -102,36 +94,54 @@ type FastifyCookiePlugin = FastifyPluginCallback<
102
94
  >;
103
95
 
104
96
  declare namespace fastifyCookie {
105
- export interface Signer {
106
- sign: (input: string) => string;
107
- unsign: (input: string) => {
108
- valid: boolean;
109
- renew: boolean;
110
- value: string | null;
111
- };
97
+ interface SignerBase {
98
+ sign: (value: string) => string;
99
+ unsign: (input: string) => UnsignResult;
100
+ }
101
+
102
+ export class Signer implements SignerBase {
103
+ constructor (secrets: string | Array<string>, algorithm?: string)
104
+ sign: (value: string) => string;
105
+ unsign: (input: string) => UnsignResult;
112
106
  }
113
107
 
114
108
  export interface CookieSerializeOptions {
109
+ /** The `Domain` attribute. */
115
110
  domain?: string;
116
111
  encode?(val: string): string;
112
+ /** The expiration `date` used for the `Expires` attribute. If both `expires` and `maxAge` are set, then `expires` is used. */
117
113
  expires?: Date;
114
+ /** The `boolean` value of the `HttpOnly` attribute. Defaults to true. */
118
115
  httpOnly?: boolean;
116
+ /** A `number` in milliseconds that specifies the `Expires` attribute by adding the specified milliseconds to the current date. If both `expires` and `maxAge` are set, then `expires` is used. */
119
117
  maxAge?: number;
118
+ /** The `Path` attribute. Defaults to `/` (the root path). */
120
119
  path?: string;
121
120
  priority?: "low" | "medium" | "high";
122
- sameSite?: boolean | "lax" | "strict" | "none";
123
- secure?: boolean;
121
+ /** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `node` or `strict`. */
122
+ sameSite?: 'lax' | 'none' | 'strict' | boolean;
123
+ /** The `boolean` value of the `Secure` attribute. Set this option to false when communicating over an unencrypted (HTTP) connection. Value can be set to `auto`; in this case the `Secure` attribute will be set to false for HTTP request, in case of HTTPS it will be set to true. Defaults to true. */
124
+ secure?: boolean | 'auto';
124
125
  signed?: boolean;
125
126
  }
126
127
 
128
+ type HookType = 'onRequest' | 'preParsing' | 'preValidation' | 'preHandler' | 'preSerialization';
129
+
127
130
  export interface FastifyCookieOptions {
128
131
  secret?: string | string[] | Signer;
132
+ hook?: HookType | false;
129
133
  parseOptions?: fastifyCookie.CookieSerializeOptions;
130
134
  }
131
135
 
132
- export type Sign = (value: string, secret: string) => string;
133
- export type Unsign = (input: string, secret: string) => string | false;
134
- export type SignerFactory = (secret: string) => Signer;
136
+ export type Sign = (value: string, secret: string, algorithm?: string) => string;
137
+ export type Unsign = (input: string, secret: string, algorithm?: string) => UnsignResult;
138
+ export type SignerFactory = (secrets: string | Array<string>, algorithm?: string) => SignerBase;
139
+
140
+ export interface UnsignResult {
141
+ valid: boolean;
142
+ renew: boolean;
143
+ value: string | null;
144
+ }
135
145
 
136
146
  export const signerFactory: SignerFactory;
137
147
  export const sign: Sign;
@@ -139,6 +149,7 @@ declare namespace fastifyCookie {
139
149
 
140
150
  export interface FastifyCookie extends FastifyCookiePlugin {
141
151
  signerFactory: SignerFactory;
152
+ Signer: Signer;
142
153
  sign: Sign;
143
154
  unsign: Unsign;
144
155
  }
@@ -146,7 +157,8 @@ declare namespace fastifyCookie {
146
157
  export const fastifyCookie: FastifyCookie;
147
158
 
148
159
  export interface FastifyCookieOptions {
149
- secret?: string | string[] | Signer;
160
+ secret?: string | string[] | SignerBase;
161
+ algorithm?: string;
150
162
  parseOptions?: CookieSerializeOptions;
151
163
  }
152
164
 
@@ -157,4 +169,4 @@ declare function fastifyCookie(
157
169
  ...params: Parameters<FastifyCookiePlugin>
158
170
  ): ReturnType<FastifyCookiePlugin>;
159
171
 
160
- export = fastifyCookie;
172
+ export = fastifyCookie;
@@ -1,9 +1,10 @@
1
- import cookie from '../plugin';
2
- import { expectType } from 'tsd';
1
+ import cookie from '..';
2
+ import { expectError, expectType } from 'tsd';
3
3
  import * as fastifyCookieStar from '..';
4
4
  import fastifyCookieCjsImport = require('..');
5
5
  import fastifyCookieDefault, { fastifyCookie as fastifyCookieNamed } from '..';
6
- import fastify, { FastifyInstance, FastifyReply, setCookieWrapper } from 'fastify';
6
+ import fastify, { FastifyInstance, FastifyPluginCallback, FastifyReply, setCookieWrapper } from 'fastify';
7
+ import { Server } from 'http';
7
8
 
8
9
  const fastifyCookieCjs = require('..');
9
10
 
@@ -58,6 +59,16 @@ server.after((_err) => {
58
59
  .send({ hello: 'world' })
59
60
  );
60
61
  });
62
+
63
+ expectType<(value: string) => string>(server.signCookie)
64
+ expectType<(value: string) => fastifyCookieStar.UnsignResult>(server.unsignCookie)
65
+
66
+ server.get('/', (request, reply) => {
67
+ expectType<(value: string) => string>(request.signCookie)
68
+ expectType<(value: string) => string>(reply.signCookie)
69
+ expectType<(value: string) => fastifyCookieStar.UnsignResult>(request.unsignCookie)
70
+ expectType<(value: string) => fastifyCookieStar.UnsignResult>(reply.unsignCookie)
71
+ });
61
72
  });
62
73
 
63
74
  const serverWithHttp2 = fastify({ http2: true });
@@ -109,6 +120,10 @@ const appWithImplicitHttpSigned = fastify();
109
120
  appWithImplicitHttpSigned.register(cookie, {
110
121
  secret: 'testsecret',
111
122
  });
123
+ appWithImplicitHttpSigned.register(cookie, {
124
+ secret: 'testsecret',
125
+ algorithm: 'sha512'
126
+ });
112
127
  appWithImplicitHttpSigned.after(() => {
113
128
  server.get('/', (request, reply) => {
114
129
  appWithImplicitHttpSigned.unsignCookie(request.cookies.test!);
@@ -194,3 +209,20 @@ appWithCustomSigner.after(() => {
194
209
  reply.send({ hello: 'world' })
195
210
  })
196
211
  })
212
+
213
+ new fastifyCookieStar.Signer('secretString')
214
+ new fastifyCookieStar.Signer(['secretStringInArray'])
215
+ const signer = new fastifyCookieStar.Signer(['secretStringInArray'], 'sha256')
216
+ signer.sign('Lorem Ipsum')
217
+ signer.unsign('Lorem Ipsum')
218
+
219
+ const appWithHook: FastifyInstance = fastify();
220
+
221
+ appWithHook.register(cookie, { hook: false });
222
+ appWithHook.register(cookie, { hook: 'onRequest' });
223
+ appWithHook.register(cookie, { hook: 'preHandler' });
224
+ appWithHook.register(cookie, { hook: 'preParsing' });
225
+ appWithHook.register(cookie, { hook: 'preSerialization' });
226
+ appWithHook.register(cookie, { hook: 'preValidation' });
227
+ expectError(appWithHook.register(cookie, { hook: true }));
228
+ expectError(appWithHook.register(cookie, { hook: 'false' }));