├── .prettierrc
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── deno.jsonc
├── deno.lock
├── deps.ts
├── index.ts
├── mod.ts
├── tests
└── index.test.ts
└── utils.ts
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "tabWidth": 2,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.unstable": true
4 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ### 1.2.0
4 |
5 | Updated deps:
6 | 1. `url-safe-base64` to v1.3.0
7 | 2. `std` library to v0.181.0
8 |
9 |
10 | #### Changes
11 |
12 | * url-safe-base64 changed how to encode strings.
13 | from `foo.jX52h6RAjKebzfy6zXDfTWWmbtQLDRxtvvVVJ82clz0.`
14 | to `foo.jX52h6RAjKebzfy6zXDfTWWmbtQLDRxtvvVVJ82clz0=`
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 omar2205
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Squishy Cookies 🍪
2 |
3 |
4 |
5 | Easily sign and verify cookies.
6 |
7 |

8 |
9 |
10 |
11 | ## Usage
12 |
13 | ```ts
14 | import {
15 | createSignedCookie, verifySignedCookie,
16 | } from 'https://deno.land/x/squishy_cookies/mod.ts'
17 |
18 | const COOKIE_SECRET = Deno.env.get('COOKIE_SECRET') || 'super_secret'
19 |
20 | // 1. Create a signed cookie
21 |
22 | // Optional, headers or cookie string (to be used in Set-cookie header)
23 | const { headers, cookie } = await createSignedCookie(
24 | 'id', '1', COOKIE_SECRET,
25 | { httpOnly: true, path: '/' }
26 | )
27 | return new Response(page, { headers })
28 |
29 | // or
30 |
31 | const headers = new Headers()
32 | headers.append('set-cookie', cookie)
33 |
34 |
35 | // 2. Verifying a cookie
36 |
37 | headers.append('cookie', cookie) // verifySignedCookie will search for 'cookie' header
38 | const userId = await verifySignedCookie(headers, 'id', COOKIE_SECRET)
39 | // userId is false if the verification failed or the cookie value
40 | if (userId) {
41 | const user = await getUserById(userId.split('.')[0])
42 | }
43 |
44 | ```
45 |
46 |
47 | ### Tip to add multiple cookies
48 | ```ts
49 | const { cookie: cookie1 } = await createSignedCookie(/* ... */)
50 | const { cookie: cookie2 } = await createSignedCookie(/* ... */)
51 |
52 | const response = new Response(someHTML, {
53 | // using headers as an array of arrays let you use
54 | // multiple headers with the same name
55 | headers: [
56 | ['Set-cookie', cookie1],
57 | ['Set-cookie', cookie2],
58 | ]
59 | })
60 | ```
61 |
--------------------------------------------------------------------------------
/deno.jsonc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omar2205/squishy_cookies/8db831b890d657d25ae17ec1142a75245044fea6/deno.jsonc
--------------------------------------------------------------------------------
/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2",
3 | "remote": {
4 | "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
5 | "https://deno.land/std@0.181.0/datetime/to_imf.ts": "8f9c0af8b167031ffe2e03da01a12a3b0672cc7562f89c61942a0ab0129771b2",
6 | "https://deno.land/std@0.181.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d",
7 | "https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
8 | "https://deno.land/std@0.181.0/http/cookie.ts": "934f92d871d50852dbd7a836d721df5a9527b14381db16001b40991d30174ee4",
9 | "https://deno.land/std@0.181.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
10 | "https://deno.land/std@0.181.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
11 | "https://deno.land/std@0.181.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
12 | "https://esm.sh/url-safe-base64@1.3.0": "743f91c6d88e33ff8583bf3b7a5d66465989ae716e004a845ec3234c258b3809",
13 | "https://esm.sh/v112/url-safe-base64@1.3.0/deno/url-safe-base64.mjs": "9c5977d6c7292e9fa85cdd850e6a70af4687e82ed877ad6b3728a1c959fc80c5",
14 | "https://esm.sh/v112/url-safe-base64@1.3.0/types/index.d.ts": "8894b06c4bd98ee5a3f83a4d1b0e012b3c9efb11c13ae5f74c77de3f9212ef35"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/deps.ts:
--------------------------------------------------------------------------------
1 | export {
2 | decode as b64Decode,
3 | encode as b64Encode,
4 | } from 'https://deno.land/std@0.181.0/encoding/base64.ts'
5 |
6 | export {
7 | decode as safeDecode,
8 | encode as safeEncode,
9 | } from 'https://esm.sh/url-safe-base64@1.3.0'
10 |
11 | export {
12 | type Cookie,
13 | getCookies,
14 | setCookie,
15 | } from 'https://deno.land/std@0.181.0/http/cookie.ts'
16 |
17 | export { assertEquals } from 'https://deno.land/std@0.181.0/testing/asserts.ts'
18 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import { type Cookie, getCookies, setCookie } from './deps.ts'
2 | import { createKey, sign, verify } from './utils.ts'
3 |
4 | /**
5 | * Sign a value with Secret
6 | *
7 | * ### usage:
8 | * ```ts
9 | * const cookie = await cookieSign('hello', 'super_secret')
10 | * // hello.gsSaKanhysk-CuNkIJhUWsHItAOcFZbrNNTa95qCfAE.
11 | * ```
12 | */
13 | const cookieSign = async (value: string, secret: string) => {
14 | const key = await createKey(secret)
15 | const data = await sign(key, value)
16 | return `${value}.${data}`
17 | }
18 |
19 | /**
20 | * Verify an input with Secret
21 | *
22 | * ### usage:
23 | * ```ts
24 | * cookieVerify('hello.gsSaKanhysk-CuNkIJhUWsHItAOcFZbrNNTa95qCfAE.', 'super_secret')
25 | * // true
26 | * ```
27 | */
28 | const cookieVerify = async (input: string, secret: string) => {
29 | const key = await createKey(secret)
30 |
31 | // get the signature and raw data
32 | try {
33 | const inputArr = input.split('.')
34 |
35 | const data = inputArr.slice(0, -1).join('.')
36 | const signature = inputArr.at(-1)
37 |
38 | if (!signature) throw Error('Invalid input: Bad data')
39 |
40 | return await verify(key, signature, data)
41 | } catch (_err) {
42 | throw Error('Invalid input')
43 | }
44 | }
45 |
46 | type CookieOptions = Omit
47 |
48 | /**
49 | * Create a signed cookie. **returns** the cookie string and headers
50 | *
51 | * ### usage:
52 | * ```ts
53 | * const { cookie } = await createSignedCookie('id', '1', 'super_secret', { httpOnly: true })
54 | * // id=1....
55 | * ```
56 | */
57 | const createSignedCookie = async (
58 | cookie_name: string,
59 | cookie_value: string,
60 | secret: string,
61 | opts: CookieOptions = { path: '/' }
62 | ) => {
63 | const value = await cookieSign(cookie_value, secret)
64 |
65 | const cookie: Cookie = {
66 | name: cookie_name,
67 | value,
68 | ...opts,
69 | }
70 |
71 | const headers = new Headers()
72 | setCookie(headers, cookie)
73 | return { headers, cookie: headers.get('set-cookie') || '' }
74 | }
75 |
76 | /**
77 | * Verify a signed cookie.
78 | *
79 | * ### usage:
80 | * ```ts
81 | * await verifySignedCookie(headers, 'id', 'super_secret')
82 | * // false or cookie value
83 | * ```
84 | */
85 | const verifySignedCookie = async (
86 | headers: Headers,
87 | cookie_name: string,
88 | secret: string
89 | ) => {
90 | const cookie = getCookies(headers)[cookie_name]
91 |
92 | if (cookie && (await cookieVerify(cookie, secret))) {
93 | return cookie
94 | }
95 | return false
96 | }
97 |
98 | export { cookieSign, cookieVerify, createSignedCookie, verifySignedCookie }
99 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export { cookieSign, cookieVerify, createSignedCookie, verifySignedCookie } from './index.ts'
2 |
--------------------------------------------------------------------------------
/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from '../deps.ts'
2 | import {
3 | cookieSign,
4 | cookieVerify,
5 | createSignedCookie,
6 | verifySignedCookie,
7 | } from '../index.ts'
8 |
9 | // holds all cookie operations results
10 | const cookies_res = {
11 | hello_super_secret: await cookieSign('hello', 'super_secret'),
12 | id_1_super_secret: await cookieSign('1', 'super_secret'),
13 | id_1_extra_super_secret: `id=${await cookieSign('1', 'super_secret')}; HttpOnly; Path=/`,
14 | }
15 |
16 | Deno.test('cookie testing', async (t) => {
17 | await t.step('sign a cookie', async () => {
18 | const cookie = await cookieSign('hello', 'super_secret')
19 | assertEquals(cookie, cookies_res['hello_super_secret'])
20 | })
21 |
22 | await t.step('verify a cookie', async () => {
23 | const res = await cookieVerify(
24 | cookies_res['hello_super_secret'],
25 | 'super_secret'
26 | )
27 | assertEquals(res, true)
28 | })
29 | })
30 |
31 | Deno.test('Creating and verifying cookies', async (t) => {
32 | let cookie: string
33 |
34 | await t.step('create a signed cookie', async () => {
35 | ({ cookie } = await createSignedCookie('id', '1', 'super_secret', {
36 | httpOnly: true,
37 | path: '/',
38 | }))
39 | assertEquals(
40 | cookie,
41 | cookies_res['id_1_extra_super_secret']
42 | )
43 | })
44 |
45 | await t.step('verify a cookie', async () => {
46 | const header = new Headers()
47 | header.set('cookie', cookie)
48 | const check = await verifySignedCookie(header, 'id', 'super_secret')
49 | assertEquals(check, cookies_res['id_1_super_secret'])
50 | })
51 |
52 | await t.step('verify a cookie - false', async () => {
53 | const header = new Headers()
54 |
55 | // messing with a cookie
56 | let c = cookies_res['id_1_super_secret']
57 | c = c.slice(0, 6) + c.slice(7)
58 |
59 | header.set('cookie', c)
60 | const check = await verifySignedCookie(header, 'id', 'super_secret')
61 | assertEquals(check, false)
62 | })
63 |
64 | await t.step('verify a cookie - missing', async () => {
65 | const header = new Headers()
66 |
67 | header.set('cookie', cookies_res['id_1_super_secret'])
68 | const check = await verifySignedCookie(header, 'user_id', 'super_secret')
69 | assertEquals(check, false)
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/utils.ts:
--------------------------------------------------------------------------------
1 | import { b64Encode, b64Decode, safeEncode, safeDecode } from './deps.ts'
2 |
3 | const encoder = new TextEncoder()
4 | const decoder = new TextDecoder('utf-8')
5 | const StringToBuffer = (s: string) => encoder.encode(s)
6 |
7 | /**
8 | * Creates a CryptoKey from a secret
9 | */
10 | async function createKey(secret: string) {
11 | return await crypto.subtle.importKey(
12 | 'raw',
13 | encoder.encode(secret),
14 | { name: 'HMAC', hash: 'SHA-256' },
15 | true,
16 | ['sign', 'verify']
17 | )
18 | }
19 |
20 | /**
21 | * Sign data with key
22 | */
23 | async function sign(key: CryptoKey, data: string) {
24 | const d = await crypto.subtle.sign('HMAC', key, encoder.encode(data))
25 | return safeEncode(b64Encode(d))
26 | }
27 |
28 | /**
29 | * Use key to verify the signature (Signed) against the raw data
30 | */
31 | async function verify(key: CryptoKey, signature: string, data: string) {
32 | const signature_decoded = b64Decode(safeDecode(signature))
33 | return await crypto.subtle.verify(
34 | 'HMAC',
35 | key,
36 | signature_decoded,
37 | encoder.encode(data)
38 | )
39 | }
40 |
41 | export { encoder, decoder, StringToBuffer, createKey, sign, verify }
42 |
--------------------------------------------------------------------------------