@fastify/cookie 7.2.0 → 7.4.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.
- package/.github/workflows/ci.yml +1 -0
- package/README.md +8 -7
- package/benchmark/signer-multi.js +18 -0
- package/benchmark/signer.js +17 -0
- package/package.json +4 -4
- package/plugin.d.ts +78 -47
- package/plugin.js +24 -5
- package/signer.js +108 -24
- package/test/cookie.test.js +50 -12
- package/test/plugin.test-d.ts +28 -13
- package/test/signer.test.js +72 -17
package/.github/workflows/ci.yml
CHANGED
package/README.md
CHANGED
|
@@ -99,7 +99,8 @@ with a value of `'foo'` on the cookie path `/`.
|
|
|
99
99
|
+ `name`: a string name for the cookie to be set
|
|
100
100
|
+ `value`: a string value for the cookie
|
|
101
101
|
+ `options`: an options object as described in the [cookie serialize][cs] documentation
|
|
102
|
-
|
|
102
|
+
+ `options.signed`: the cookie should be signed
|
|
103
|
+
+ `options.secure`: if set to `true` it will set the Secure-flag. If it is set to `"auto"` Secure-flag is set when the connection is using tls.
|
|
103
104
|
|
|
104
105
|
#### Securing the cookie
|
|
105
106
|
|
|
@@ -235,12 +236,12 @@ test('Request requires signed cookie', async () => {
|
|
|
235
236
|
|
|
236
237
|
### Manual signing/unsigning with low level utilities
|
|
237
238
|
|
|
238
|
-
with
|
|
239
|
+
with Signer
|
|
239
240
|
|
|
240
241
|
```js
|
|
241
|
-
const {
|
|
242
|
+
const { Signer } = require('@fastify/cookie');
|
|
242
243
|
|
|
243
|
-
const signer =
|
|
244
|
+
const signer = new Signer('secret');
|
|
244
245
|
const signedValue = signer.sign('test');
|
|
245
246
|
const {valid, renew, value } = signer.unsign(signedValue);
|
|
246
247
|
```
|
|
@@ -248,10 +249,10 @@ const {valid, renew, value } = signer.unsign(signedValue);
|
|
|
248
249
|
with sign/unsign utilities
|
|
249
250
|
|
|
250
251
|
```js
|
|
251
|
-
const {
|
|
252
|
+
const { fastifyCookie } = require('@fastify/cookie');
|
|
252
253
|
|
|
253
|
-
const signedValue = sign('test', 'secret');
|
|
254
|
-
const unsignedvalue = unsign(signedValue, 'secret');
|
|
254
|
+
const signedValue = fastifyCookie.sign('test', 'secret');
|
|
255
|
+
const unsignedvalue = fastifyCookie.unsign(signedValue, 'secret');
|
|
255
256
|
```
|
|
256
257
|
|
|
257
258
|
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/cookie",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "Plugin for fastify to add support for cookies",
|
|
5
5
|
"main": "plugin.js",
|
|
6
6
|
"types": "plugin.d.ts",
|
|
@@ -39,9 +39,10 @@
|
|
|
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",
|
|
44
|
+
"benchmark": "^2.1.4",
|
|
43
45
|
"fastify": "^4.0.0-rc.2",
|
|
44
|
-
"pre-commit": "^1.2.2",
|
|
45
46
|
"sinon": "^14.0.0",
|
|
46
47
|
"snazzy": "^9.0.0",
|
|
47
48
|
"standard": "^17.0.0",
|
|
@@ -51,8 +52,7 @@
|
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"cookie": "^0.5.0",
|
|
54
|
-
"
|
|
55
|
-
"fastify-plugin": "^3.0.1"
|
|
55
|
+
"fastify-plugin": "^4.0.0"
|
|
56
56
|
},
|
|
57
57
|
"tsd": {
|
|
58
58
|
"directory": "test"
|
package/plugin.d.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
/// <reference types='node' />
|
|
2
2
|
|
|
3
|
-
import { FastifyPluginCallback } from
|
|
3
|
+
import { FastifyPluginCallback } from "fastify";
|
|
4
4
|
|
|
5
|
-
declare module
|
|
5
|
+
declare module "fastify" {
|
|
6
6
|
interface FastifyInstance {
|
|
7
7
|
/**
|
|
8
8
|
* Unsigns the specified cookie using the secret provided.
|
|
9
9
|
* @param value Cookie value
|
|
10
10
|
*/
|
|
11
|
-
unsignCookie(value: string):
|
|
12
|
-
valid: boolean;
|
|
13
|
-
renew: boolean;
|
|
14
|
-
value: string | null;
|
|
15
|
-
};
|
|
11
|
+
unsignCookie(value: string): fastifyCookie.UnsignResult;
|
|
16
12
|
/**
|
|
17
13
|
* Manual cookie parsing method
|
|
18
14
|
* @docs https://github.com/fastify/fastify-cookie#manual-cookie-parsing
|
|
@@ -39,17 +35,13 @@ declare module 'fastify' {
|
|
|
39
35
|
* Unsigns the specified cookie using the secret provided.
|
|
40
36
|
* @param value Cookie value
|
|
41
37
|
*/
|
|
42
|
-
unsignCookie(value: string):
|
|
43
|
-
valid: boolean;
|
|
44
|
-
renew: boolean;
|
|
45
|
-
value: string | null;
|
|
46
|
-
};
|
|
38
|
+
unsignCookie(value: string): fastifyCookie.UnsignResult;
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
export type setCookieWrapper = (
|
|
50
42
|
name: string,
|
|
51
43
|
value: string,
|
|
52
|
-
options?: CookieSerializeOptions
|
|
44
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
53
45
|
) => FastifyReply;
|
|
54
46
|
|
|
55
47
|
interface FastifyReply {
|
|
@@ -63,65 +55,104 @@ declare module 'fastify' {
|
|
|
63
55
|
setCookie(
|
|
64
56
|
name: string,
|
|
65
57
|
value: string,
|
|
66
|
-
options?: CookieSerializeOptions
|
|
58
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
67
59
|
): this;
|
|
68
60
|
|
|
69
61
|
/**
|
|
70
62
|
* @alias setCookie
|
|
71
63
|
*/
|
|
72
|
-
cookie(
|
|
64
|
+
cookie(
|
|
65
|
+
name: string,
|
|
66
|
+
value: string,
|
|
67
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
68
|
+
): this;
|
|
73
69
|
|
|
74
70
|
/**
|
|
75
71
|
* clear response cookie
|
|
76
72
|
* @param name Cookie name
|
|
77
73
|
* @param options Serialize options
|
|
78
74
|
*/
|
|
79
|
-
clearCookie(
|
|
75
|
+
clearCookie(
|
|
76
|
+
name: string,
|
|
77
|
+
options?: fastifyCookie.CookieSerializeOptions
|
|
78
|
+
): this;
|
|
80
79
|
|
|
81
80
|
/**
|
|
82
81
|
* Unsigns the specified cookie using the secret provided.
|
|
83
82
|
* @param value Cookie value
|
|
84
83
|
*/
|
|
85
|
-
unsignCookie(value: string):
|
|
86
|
-
valid: boolean;
|
|
87
|
-
renew: boolean;
|
|
88
|
-
value: string | null;
|
|
89
|
-
};
|
|
84
|
+
unsignCookie(value: string): fastifyCookie.UnsignResult;
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
88
|
+
type FastifyCookiePlugin = FastifyPluginCallback<
|
|
89
|
+
NonNullable<fastifyCookie.FastifyCookieOptions>
|
|
90
|
+
>;
|
|
91
|
+
|
|
92
|
+
declare namespace fastifyCookie {
|
|
93
|
+
interface SignerBase {
|
|
94
|
+
sign: (value: string) => string;
|
|
95
|
+
unsign: (input: string) => UnsignResult;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class Signer implements SignerBase {
|
|
99
|
+
constructor (secrets: string | Array<string>, algorithm?: string)
|
|
100
|
+
sign: (value: string) => string;
|
|
101
|
+
unsign: (input: string) => UnsignResult;
|
|
102
|
+
}
|
|
105
103
|
|
|
106
|
-
interface
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
export interface CookieSerializeOptions {
|
|
105
|
+
domain?: string;
|
|
106
|
+
encode?(val: string): string;
|
|
107
|
+
expires?: Date;
|
|
108
|
+
httpOnly?: boolean;
|
|
109
|
+
maxAge?: number;
|
|
110
|
+
path?: string;
|
|
111
|
+
priority?: "low" | "medium" | "high";
|
|
112
|
+
sameSite?: boolean | "lax" | "strict" | "none";
|
|
113
|
+
secure?: boolean;
|
|
114
|
+
signed?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface FastifyCookieOptions {
|
|
118
|
+
secret?: string | string[] | Signer;
|
|
119
|
+
parseOptions?: fastifyCookie.CookieSerializeOptions;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type Sign = (value: string, secret: string, algorithm?: string) => string;
|
|
123
|
+
export type Unsign = (input: string, secret: string, algorithm?: string) => UnsignResult;
|
|
124
|
+
export type SignerFactory = (secrets: string | Array<string>, algorithm?: string) => SignerBase;
|
|
125
|
+
|
|
126
|
+
export interface UnsignResult {
|
|
109
127
|
valid: boolean;
|
|
110
128
|
renew: boolean;
|
|
111
129
|
value: string | null;
|
|
112
|
-
}
|
|
113
|
-
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const signerFactory: SignerFactory;
|
|
133
|
+
export const sign: Sign;
|
|
134
|
+
export const unsign: Unsign;
|
|
135
|
+
|
|
136
|
+
export interface FastifyCookie extends FastifyCookiePlugin {
|
|
137
|
+
signerFactory: SignerFactory;
|
|
138
|
+
Signer: Signer;
|
|
139
|
+
sign: Sign;
|
|
140
|
+
unsign: Unsign;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const fastifyCookie: FastifyCookie;
|
|
114
144
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
export interface FastifyCookieOptions {
|
|
146
|
+
secret?: string | string[] | SignerBase;
|
|
147
|
+
algorithm?: string;
|
|
148
|
+
parseOptions?: CookieSerializeOptions;
|
|
149
|
+
}
|
|
118
150
|
|
|
119
|
-
export
|
|
120
|
-
secret?: string | string[] | Signer;
|
|
121
|
-
parseOptions?: CookieSerializeOptions;
|
|
151
|
+
export { fastifyCookie as default };
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
declare
|
|
154
|
+
declare function fastifyCookie(
|
|
155
|
+
...params: Parameters<FastifyCookiePlugin>
|
|
156
|
+
): ReturnType<FastifyCookiePlugin>;
|
|
125
157
|
|
|
126
|
-
export
|
|
127
|
-
export { fastifyCookie, signerFactory, sign, unsign };
|
|
158
|
+
export = fastifyCookie;
|
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
|
|
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)
|
|
@@ -16,6 +15,15 @@ function fastifyCookieSetCookie (reply, name, value, options, signer) {
|
|
|
16
15
|
value = signer.sign(value)
|
|
17
16
|
}
|
|
18
17
|
|
|
18
|
+
if (opts.secure === 'auto') {
|
|
19
|
+
if (isConnectionSecure(reply.request)) {
|
|
20
|
+
opts.secure = true
|
|
21
|
+
} else {
|
|
22
|
+
opts.sameSite = 'lax'
|
|
23
|
+
opts.secure = false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
const serialized = cookie.serialize(name, value, opts)
|
|
20
28
|
let setCookie = reply.getHeader('Set-Cookie')
|
|
21
29
|
if (!setCookie) {
|
|
@@ -56,7 +64,8 @@ function onReqHandlerWrapper (fastify) {
|
|
|
56
64
|
function plugin (fastify, options, next) {
|
|
57
65
|
const secret = options.secret || ''
|
|
58
66
|
const enableRotation = Array.isArray(secret)
|
|
59
|
-
const
|
|
67
|
+
const algorithm = options.algorithm || 'sha256'
|
|
68
|
+
const signer = typeof secret === 'string' || enableRotation ? new Signer(secret, algorithm) : secret
|
|
60
69
|
|
|
61
70
|
fastify.decorate('parseCookie', parseCookie)
|
|
62
71
|
fastify.decorate('signCookie', signCookie)
|
|
@@ -96,6 +105,13 @@ function plugin (fastify, options, next) {
|
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
function isConnectionSecure (request) {
|
|
109
|
+
return (
|
|
110
|
+
request.raw.socket?.encrypted === true ||
|
|
111
|
+
request.headers['x-forwarded-proto'] === 'https'
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
const fastifyCookie = fp(plugin, {
|
|
100
116
|
fastify: '4.x',
|
|
101
117
|
name: '@fastify/cookie'
|
|
@@ -111,14 +127,17 @@ const fastifyCookie = fp(plugin, {
|
|
|
111
127
|
* - `import { fastifyCookie } from 'fastify-cookie'`
|
|
112
128
|
* - `import fastifyCookie from 'fastify-cookie'`
|
|
113
129
|
*/
|
|
130
|
+
fastifyCookie.signerFactory = Signer
|
|
114
131
|
fastifyCookie.fastifyCookie = fastifyCookie
|
|
115
132
|
fastifyCookie.default = fastifyCookie
|
|
116
133
|
module.exports = fastifyCookie
|
|
117
134
|
|
|
118
|
-
fastifyCookie.fastifyCookie.signerFactory =
|
|
135
|
+
fastifyCookie.fastifyCookie.signerFactory = Signer
|
|
136
|
+
fastifyCookie.fastifyCookie.Signer = Signer
|
|
119
137
|
fastifyCookie.fastifyCookie.sign = sign
|
|
120
138
|
fastifyCookie.fastifyCookie.unsign = unsign
|
|
121
139
|
|
|
122
|
-
module.exports.signerFactory =
|
|
140
|
+
module.exports.signerFactory = Signer
|
|
141
|
+
module.exports.Signer = Signer
|
|
123
142
|
module.exports.sign = sign
|
|
124
143
|
module.exports.unsign = unsign
|
package/signer.js
CHANGED
|
@@ -1,32 +1,116 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
const secrets = Array.isArray(secret) ? secret : [secret]
|
|
7
|
-
const [signingKey] = secrets
|
|
9
|
+
const crypto = require('crypto')
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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, 'dummyHmac')
|
|
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
|
package/test/cookie.test.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
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=${
|
|
584
|
+
cookie: `foo=${sign('foo', 'invalid-secret')}`
|
|
585
585
|
}
|
|
586
586
|
}, (err, res) => {
|
|
587
587
|
t.error(err)
|
|
@@ -776,3 +776,41 @@ test('create signed cookie manually using signCookie decorator', async (t) => {
|
|
|
776
776
|
t.equal(res.statusCode, 200)
|
|
777
777
|
t.same(JSON.parse(res.body), { unsigned: { value: 'bar', renew: false, valid: true } })
|
|
778
778
|
})
|
|
779
|
+
|
|
780
|
+
test('handle secure:auto of cookieOptions', async (t) => {
|
|
781
|
+
const fastify = Fastify()
|
|
782
|
+
|
|
783
|
+
await fastify.register(plugin)
|
|
784
|
+
|
|
785
|
+
fastify.get('/test1', (req, reply) => {
|
|
786
|
+
reply
|
|
787
|
+
.setCookie('foo', 'foo', { path: '/', secure: 'auto' })
|
|
788
|
+
.send()
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const res = await fastify.inject({
|
|
792
|
+
method: 'GET',
|
|
793
|
+
url: '/test1',
|
|
794
|
+
headers: { 'x-forwarded-proto': 'https' }
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
const cookies = res.cookies
|
|
798
|
+
t.equal(cookies.length, 1)
|
|
799
|
+
t.equal(cookies[0].name, 'foo')
|
|
800
|
+
t.equal(cookies[0].value, 'foo')
|
|
801
|
+
t.equal(cookies[0].secure, true)
|
|
802
|
+
t.equal(cookies[0].path, '/')
|
|
803
|
+
|
|
804
|
+
const res2 = await fastify.inject({
|
|
805
|
+
method: 'GET',
|
|
806
|
+
url: '/test1'
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
const cookies2 = res2.cookies
|
|
810
|
+
t.equal(cookies2.length, 1)
|
|
811
|
+
t.equal(cookies2[0].name, 'foo')
|
|
812
|
+
t.equal(cookies2[0].value, 'foo')
|
|
813
|
+
t.equal(cookies2[0].sameSite, 'Lax')
|
|
814
|
+
t.same(cookies2[0].secure, null)
|
|
815
|
+
t.equal(cookies2[0].path, '/')
|
|
816
|
+
})
|
package/test/plugin.test-d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Server } from 'http';
|
|
1
|
+
import cookie from '../plugin';
|
|
3
2
|
import { expectType } from 'tsd';
|
|
4
3
|
import * as fastifyCookieStar from '..';
|
|
5
|
-
import fastifyCookieDefault, {
|
|
6
|
-
fastifyCookie as fastifyCookieNamed
|
|
7
|
-
} from '..';
|
|
8
|
-
import cookie, { FastifyCookieOptions } from '../plugin';
|
|
9
|
-
|
|
10
4
|
import fastifyCookieCjsImport = require('..');
|
|
5
|
+
import fastifyCookieDefault, { fastifyCookie as fastifyCookieNamed } from '..';
|
|
6
|
+
import fastify, { FastifyInstance, FastifyReply, setCookieWrapper } from 'fastify';
|
|
7
|
+
|
|
11
8
|
const fastifyCookieCjs = require('..');
|
|
12
9
|
|
|
13
10
|
const app: FastifyInstance = fastify();
|
|
@@ -19,16 +16,24 @@ app.register(fastifyCookieCjsImport.fastifyCookie);
|
|
|
19
16
|
app.register(fastifyCookieStar.default);
|
|
20
17
|
app.register(fastifyCookieStar.fastifyCookie);
|
|
21
18
|
|
|
22
|
-
expectType<
|
|
23
|
-
expectType<
|
|
24
|
-
expectType<
|
|
25
|
-
expectType<
|
|
26
|
-
expectType<
|
|
27
|
-
expectType<
|
|
19
|
+
expectType<fastifyCookieStar.FastifyCookie>(fastifyCookieNamed);
|
|
20
|
+
expectType<fastifyCookieStar.FastifyCookie>(fastifyCookieDefault);
|
|
21
|
+
expectType<fastifyCookieStar.FastifyCookie>(fastifyCookieCjsImport.default);
|
|
22
|
+
expectType<fastifyCookieStar.FastifyCookie>(fastifyCookieCjsImport.fastifyCookie);
|
|
23
|
+
expectType<fastifyCookieStar.FastifyCookie>(fastifyCookieStar.default);
|
|
24
|
+
expectType<fastifyCookieStar.FastifyCookie>(
|
|
28
25
|
fastifyCookieStar.fastifyCookie
|
|
29
26
|
);
|
|
30
27
|
expectType<any>(fastifyCookieCjs);
|
|
31
28
|
|
|
29
|
+
expectType<fastifyCookieStar.Sign>(fastifyCookieDefault.sign);
|
|
30
|
+
expectType<fastifyCookieStar.Unsign>(fastifyCookieDefault.unsign);
|
|
31
|
+
expectType<fastifyCookieStar.SignerFactory >(fastifyCookieDefault.signerFactory );
|
|
32
|
+
|
|
33
|
+
expectType<fastifyCookieStar.Sign>(fastifyCookieNamed.sign);
|
|
34
|
+
expectType<fastifyCookieStar.Unsign>(fastifyCookieNamed.unsign);
|
|
35
|
+
expectType<fastifyCookieStar.SignerFactory >(fastifyCookieNamed.signerFactory );
|
|
36
|
+
|
|
32
37
|
const server = fastify();
|
|
33
38
|
|
|
34
39
|
server.register(cookie);
|
|
@@ -104,6 +109,10 @@ const appWithImplicitHttpSigned = fastify();
|
|
|
104
109
|
appWithImplicitHttpSigned.register(cookie, {
|
|
105
110
|
secret: 'testsecret',
|
|
106
111
|
});
|
|
112
|
+
appWithImplicitHttpSigned.register(cookie, {
|
|
113
|
+
secret: 'testsecret',
|
|
114
|
+
algorithm: 'sha512'
|
|
115
|
+
});
|
|
107
116
|
appWithImplicitHttpSigned.after(() => {
|
|
108
117
|
server.get('/', (request, reply) => {
|
|
109
118
|
appWithImplicitHttpSigned.unsignCookie(request.cookies.test!);
|
|
@@ -189,3 +198,9 @@ appWithCustomSigner.after(() => {
|
|
|
189
198
|
reply.send({ hello: 'world' })
|
|
190
199
|
})
|
|
191
200
|
})
|
|
201
|
+
|
|
202
|
+
new fastifyCookieStar.Signer('secretString')
|
|
203
|
+
new fastifyCookieStar.Signer(['secretStringInArray'])
|
|
204
|
+
const signer = new fastifyCookieStar.Signer(['secretStringInArray'], 'sha256')
|
|
205
|
+
signer.sign('Lorem Ipsum')
|
|
206
|
+
signer.unsign('Lorem Ipsum')
|
package/test/signer.test.js
CHANGED
|
@@ -2,33 +2,67 @@
|
|
|
2
2
|
|
|
3
3
|
const { test } = require('tap')
|
|
4
4
|
const sinon = require('sinon')
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const crypto = require('crypto')
|
|
6
|
+
const { Signer, sign, unsign } = require('../signer')
|
|
7
7
|
|
|
8
|
-
test('default',
|
|
9
|
-
t.plan(
|
|
8
|
+
test('default', t => {
|
|
9
|
+
t.plan(5)
|
|
10
10
|
|
|
11
11
|
const secret = 'my-secret'
|
|
12
|
-
const signer =
|
|
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,
|
|
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('
|
|
30
|
+
t.test('sign', (t) => {
|
|
24
31
|
t.plan(3)
|
|
25
32
|
|
|
26
|
-
const input =
|
|
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 =
|
|
41
|
-
const
|
|
74
|
+
const signer = Signer([secret1, secret2, secret3])
|
|
75
|
+
const signSpy = sinon.spy(crypto, 'createHmac')
|
|
42
76
|
|
|
43
77
|
t.beforeEach(() => {
|
|
44
|
-
|
|
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,
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
})
|