Skip to content

Commit bcc81ef

Browse files
committed
feat: add support for cookie client
Cookie client exposes the API to define/parse cookies as a client
1 parent a0bdbc9 commit bcc81ef

File tree

10 files changed

+240
-20
lines changed

10 files changed

+240
-20
lines changed

adonis-typings/container.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
declare module '@ioc:Adonis/Core/Application' {
1111
import { RouterContract } from '@ioc:Adonis/Core/Route'
1212
import { ServerContract } from '@ioc:Adonis/Core/Server'
13+
import { CookieClientContract } from '@ioc:Adonis/Core/CookieClient'
1314
import { RequestConstructorContract } from '@ioc:Adonis/Core/Request'
1415
import { ResponseConstructorContract } from '@ioc:Adonis/Core/Response'
1516
import { HttpContextConstructorContract } from '@ioc:Adonis/Core/HttpContext'
1617

1718
export interface ContainerBindings {
1819
'Adonis/Core/Route': RouterContract
1920
'Adonis/Core/Server': ServerContract
21+
'Adonis/Core/CookieClient': CookieClientContract
2022
'Adonis/Core/Request': RequestConstructorContract
2123
'Adonis/Core/Response': ResponseConstructorContract
2224
'Adonis/Core/HttpContext': HttpContextConstructorContract

adonis-typings/cookie-client.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* @adonisjs/http-server
3+
*
4+
* (c) Harminder Virk <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
declare module '@ioc:Adonis/Core/CookieClient' {
11+
export interface CookieClientContract {
12+
/**
13+
* Encrypt a key value pair to be sent in the cookie header
14+
*/
15+
encrypt(key: string, value: any): string | null
16+
17+
/**
18+
* Sign a key value pair to be sent in the cookie header
19+
*/
20+
sign(key: string, value: any): string | null
21+
22+
/**
23+
* Encode a key value pair to be sent in the cookie header
24+
*/
25+
encode(key: string, value: any): string | null
26+
27+
/**
28+
* Parse the set-cookie header
29+
*/
30+
parse(setCookieHeader: string): {
31+
name: string
32+
value: any
33+
encrypted: boolean
34+
signed: boolean
35+
path?: string
36+
expires?: Date
37+
maxAge?: number
38+
domain?: string
39+
secure?: boolean
40+
httpOnly?: boolean
41+
sameSite?: 'lax' | 'none' | 'strict'
42+
}[]
43+
44+
/**
45+
* Unsign a signed cookie value
46+
*/
47+
unsign(key: string, value: string): any
48+
49+
/**
50+
* Decrypt an encrypted cookie value
51+
*/
52+
decrypt(key: string, value: string): any
53+
54+
/**
55+
* Decode an encoded cookie value
56+
*/
57+
decode(key: string, value: string): any
58+
}
59+
60+
const CookieClient: CookieClientContract
61+
export default CookieClient
62+
}

adonis-typings/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
/// <reference path="./request.ts" />
1515
/// <reference path="./response.ts" />
1616
/// <reference path="./route.ts" />
17+
/// <reference path="./cookie-client.ts" />

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"pluralize": "^8.0.0",
109109
"proxy-addr": "^2.0.7",
110110
"qs": "^6.10.1",
111+
"set-cookie-parser": "^2.4.8",
111112
"tmp-cache": "^1.1.0",
112113
"type-is": "^1.6.18",
113114
"vary": "^1.1.2"

providers/HttpServerProvider.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import { ApplicationContract } from '@ioc:Adonis/Core/Application'
1313
export default class HttpServerProvider {
1414
constructor(protected application: ApplicationContract) {}
1515

16-
public static needsApplication = true
17-
1816
/**
1917
* Validate server config to ensure we start with sane defaults
2018
*/
@@ -90,6 +88,17 @@ export default class HttpServerProvider {
9088
})
9189
}
9290

91+
/**
92+
* Registers the cookie client with the container
93+
*/
94+
protected registerCookieClient() {
95+
this.application.container.singleton('Adonis/Core/CookieClient', () => {
96+
const { CookieClient } = require('../src/Cookie/Client')
97+
const Encryption = this.application.container.resolveBinding('Adonis/Core/Encryption')
98+
return new CookieClient(Encryption)
99+
})
100+
}
101+
93102
/**
94103
* Registering all bindings
95104
*/
@@ -99,5 +108,6 @@ export default class HttpServerProvider {
99108
this.registerHttpServer()
100109
this.registerHTTPContext()
101110
this.registerRouter()
111+
this.registerCookieClient()
102112
}
103113
}

src/Cookie/Client/index.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* @adonisjs/http-server
3+
*
4+
* (c) Harminder Virk <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
/// <reference path="../../../adonis-typings/index.ts" />
11+
12+
import setCookieParser from 'set-cookie-parser'
13+
import { EncryptionContract } from '@ioc:Adonis/Core/Encryption'
14+
import { CookieClientContract } from '@ioc:Adonis/Core/CookieClient'
15+
16+
import * as PlainCookie from '../Drivers/Plain'
17+
import * as SignedCookie from '../Drivers/Signed'
18+
import * as EncryptedCookie from '../Drivers/Encrypted'
19+
20+
/**
21+
* Cookie client exposes the API to parse/set AdonisJS
22+
* cookies as a client.
23+
*/
24+
export class CookieClient implements CookieClientContract {
25+
constructor(private encryption: EncryptionContract) {}
26+
27+
/**
28+
* Encrypt a key value pair to be sent in the cookie header
29+
*/
30+
public encrypt(key: string, value: any): string | null {
31+
return EncryptedCookie.pack(key, value, this.encryption)
32+
}
33+
34+
/**
35+
* Sign a key value pair to be sent in the cookie header
36+
*/
37+
public sign(key: string, value: any): string | null {
38+
return SignedCookie.pack(key, value, this.encryption)
39+
}
40+
41+
/**
42+
* Encode a key value pair to be sent in the cookie header
43+
*/
44+
public encode(_: string, value: any): string | null {
45+
return PlainCookie.pack(value)
46+
}
47+
48+
/**
49+
* Unsign a signed cookie value
50+
*/
51+
public unsign(key: string, value: string) {
52+
return SignedCookie.canUnpack(value) ? SignedCookie.unpack(key, value, this.encryption) : null
53+
}
54+
55+
/**
56+
* Decrypt an encrypted cookie value
57+
*/
58+
public decrypt(key: string, value: string) {
59+
return EncryptedCookie.canUnpack(value)
60+
? EncryptedCookie.unpack(key, value, this.encryption)
61+
: null
62+
}
63+
64+
/**
65+
* Decode an encoded cookie value
66+
*/
67+
public decode(_: string, value: string) {
68+
return PlainCookie.canUnpack(value) ? PlainCookie.unpack(value) : null
69+
}
70+
71+
/**
72+
* Parses the set-cookie header and returns an
73+
* array of parsed cookies
74+
*/
75+
public parse(setCookieHeader: string) {
76+
const cookies = setCookieParser(setCookieHeader)
77+
78+
return cookies.map((cookie: any) => {
79+
cookie.encrypted = false
80+
cookie.signed = false
81+
const value = cookie.value
82+
83+
/**
84+
* Unsign signed cookie
85+
*/
86+
if (SignedCookie.canUnpack(value)) {
87+
cookie.value = SignedCookie.unpack(cookie.name, value, this.encryption)
88+
cookie.signed = true
89+
return cookie
90+
}
91+
92+
/**
93+
* Decrypted encrypted cookie
94+
*/
95+
if (EncryptedCookie.canUnpack(value)) {
96+
cookie.value = EncryptedCookie.unpack(cookie.name, value, this.encryption)
97+
cookie.encrypted = true
98+
return cookie
99+
}
100+
101+
/**
102+
* Decode encoded cookie
103+
*/
104+
if (PlainCookie.canUnpack(value)) {
105+
cookie.value = PlainCookie.unpack(value)
106+
}
107+
108+
return cookie
109+
})
110+
}
111+
}

src/Cookie/Parser/index.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
import cookie from 'cookie'
1111
import { EncryptionContract } from '@ioc:Adonis/Core/Encryption'
1212

13-
import * as PlainCookie from '../Drivers/Plain'
14-
import * as SignedCookie from '../Drivers/Signed'
15-
import * as EncryptedCookie from '../Drivers/Encrypted'
13+
import { CookieClient } from '../Client'
1614

1715
/**
1816
* Cookie parser parses the HTTP `cookie` header and collects all cookies
@@ -25,6 +23,8 @@ import * as EncryptedCookie from '../Drivers/Encrypted'
2523
* server.
2624
*/
2725
export class CookieParser {
26+
private client = new CookieClient(this.encryption)
27+
2828
/**
2929
* A copy of cached cookies, they are cached during a request after
3030
* initial decoding, unsigning or decrypting.
@@ -96,7 +96,7 @@ export class CookieParser {
9696
* Attempt to unpack and cache it for future. The value is only
9797
* when value it is not null.
9898
*/
99-
const parsed = PlainCookie.canUnpack(value) ? PlainCookie.unpack(value) : null
99+
const parsed = this.client.decode(key, value)
100100
if (parsed !== null) {
101101
cacheObject[key] = parsed
102102
}
@@ -135,10 +135,7 @@ export class CookieParser {
135135
* Attempt to unpack and cache it for future. The value is only
136136
* when value it is not null.
137137
*/
138-
const parsed = SignedCookie.canUnpack(value)
139-
? SignedCookie.unpack(key, value, this.encryption)
140-
: null
141-
138+
const parsed = this.client.unsign(key, value)
142139
if (parsed !== null) {
143140
cacheObject[key] = parsed
144141
}
@@ -177,10 +174,7 @@ export class CookieParser {
177174
* Attempt to unpack and cache it for future. The value is only
178175
* when value it is not null.
179176
*/
180-
const parsed = EncryptedCookie.canUnpack(value)
181-
? EncryptedCookie.unpack(key, value, this.encryption)
182-
: null
183-
177+
const parsed = this.client.decrypt(key, value)
184178
if (parsed !== null) {
185179
cacheObject[key] = parsed
186180
}

src/Cookie/Serializer/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ import cookie, { CookieSerializeOptions } from 'cookie'
1212
import { CookieOptions } from '@ioc:Adonis/Core/Response'
1313
import { EncryptionContract } from '@ioc:Adonis/Core/Encryption'
1414

15-
import * as PlainCookie from '../Drivers/Plain'
16-
import * as SignedCookie from '../Drivers/Signed'
17-
import * as EncryptedCookie from '../Drivers/Encrypted'
15+
import { CookieClient } from '../Client'
1816

1917
/**
2018
* Cookies serializer is used to serialize a value to be set on the `Set-Cookie`
2119
* header. You can `encode`, `sign` on `encrypt` cookies using the serializer
2220
* and then set them individually using the `set-cookie` header.
2321
*/
2422
export class CookieSerializer {
23+
private client = new CookieClient(this.encryption)
24+
2525
constructor(private encryption: EncryptionContract) {}
2626

2727
/**
@@ -59,7 +59,7 @@ export class CookieSerializer {
5959
* ```
6060
*/
6161
public encode(key: string, value: any, options?: Partial<CookieOptions>): string | null {
62-
const packedValue = PlainCookie.pack(value)
62+
const packedValue = this.client.encode(key, value)
6363
if (packedValue === null) {
6464
return null
6565
}
@@ -72,7 +72,7 @@ export class CookieSerializer {
7272
* has a verification hash attached to it to detect data tampering.
7373
*/
7474
public sign(key: string, value: any, options?: Partial<CookieOptions>): string | null {
75-
const packedValue = SignedCookie.pack(key, value, this.encryption)
75+
const packedValue = this.client.sign(key, value)
7676
if (packedValue === null) {
7777
return null
7878
}
@@ -84,10 +84,11 @@ export class CookieSerializer {
8484
* Encrypts the value and returns it back as a url safe string.
8585
*/
8686
public encrypt(key: string, value: any, options?: Partial<CookieOptions>): string | null {
87-
const packedValue = EncryptedCookie.pack(key, value, this.encryption)
87+
const packedValue = this.client.encrypt(key, value)
8888
if (packedValue === null) {
8989
return null
9090
}
91+
9192
return this.serializeAsCookie(key, packedValue, options)
9293
}
9394
}

test/cookie-client.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* @adonisjs/http-server
3+
*
4+
* (c) Harminder Virk <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import test from 'japa'
11+
import { encryption } from '../test-helpers'
12+
import { CookieClient } from '../src/Cookie/Client'
13+
14+
test.group('Cookie Client', () => {
15+
test('sign cookie using cookie client', async (assert) => {
16+
const client = new CookieClient(encryption)
17+
const signed = client.sign('user_id', 1)!
18+
19+
assert.isTrue(signed.startsWith('s:'))
20+
assert.equal(client.unsign('user_id', signed), 1)
21+
})
22+
23+
test('encrypt cookie using cookie client', async (assert) => {
24+
const client = new CookieClient(encryption)
25+
const encrypted = client.encrypt('user_id', 1)!
26+
27+
assert.isTrue(encrypted.startsWith('e:'))
28+
assert.equal(client.decrypt('user_id', encrypted), 1)
29+
})
30+
31+
test('encode cookie using cookie client', async (assert) => {
32+
const client = new CookieClient(encryption)
33+
const encoded = client.encode('user_id', 1)!
34+
assert.equal(client.decode('user_id', encoded), 1)
35+
})
36+
})

test/http-server-provider.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Request } from '../src/Request'
1515
import { Response } from '../src/Response'
1616
import { fs, setupApp } from '../test-helpers'
1717
import { HttpContext } from '../src/HttpContext'
18+
import { CookieClient } from '../src/Cookie/Client'
1819
import { MiddlewareStore } from '../src/MiddlewareStore'
1920

2021
test.group('Http Server Provider', (group) => {
@@ -29,6 +30,7 @@ test.group('Http Server Provider', (group) => {
2930
assert.deepEqual(app.container.use('Adonis/Core/Request'), Request)
3031
assert.deepEqual(app.container.use('Adonis/Core/Response'), Response)
3132
assert.instanceOf(app.container.use('Adonis/Core/Server'), Server)
33+
assert.instanceOf(app.container.use('Adonis/Core/CookieClient'), CookieClient)
3234
assert.deepEqual(app.container.use('Adonis/Core/MiddlewareStore'), MiddlewareStore)
3335
assert.deepEqual(app.container.use('Adonis/Core/HttpContext'), HttpContext)
3436
})

0 commit comments

Comments
 (0)