├── .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 | --------------------------------------------------------------------------------