Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stateCookie): add state cookie #4

Merged
merged 6 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 2 additions & 83 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"test": "vitest"
},
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
56 changes: 56 additions & 0 deletions src/lib/__tests__/cookie.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, test, expect, beforeEach } from "vitest"
import { saveCookie, getCookie, deleteCookie } from "../cookie"

describe("saveCookie", () => {
test("throws error on empty name", () => {
expect(() => saveCookie())
.toThrowError("Cookie name is required")
})

test("saves empty value", () => {
saveCookie("a", "")
expect(document.cookie).toMatch("a=")
})

test("saves cookie", () => {
saveCookie("a", "1234")
expect(document.cookie).toMatch("a=1234")
})

test("saves cookie with latest value", () => {
saveCookie("a", "1234")
saveCookie("a", "124")
expect(document.cookie).toMatch("a=124")
expect(document.cookie).not.toMatch("a=1234")
})
})

describe("getCookie", () => {
test("gets none", () => {
saveCookie("b", "54321")
expect(getCookie("c")).toBeUndefined()
})

test("gets cookie", () => {
saveCookie("b", "54321")
expect(getCookie("b")).toBe("54321")
})
})

describe("deleteCookie", () => {
test("deletes cookie", () => {
saveCookie("c", "12345")
saveCookie("d", "54321")
deleteCookie("c")
expect(document.cookie).not.toMatch("c=12345")
expect(document.cookie).toMatch("d=54321")
})

test("deletes none", () => {
saveCookie("one", "12345")
saveCookie("two", "54321")
deleteCookie("e")
expect(document.cookie).toMatch("one=12345")
expect(document.cookie).toMatch("two=54321")
})
})
39 changes: 39 additions & 0 deletions src/lib/__tests__/stateCookie.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, test, expect } from "vitest"
import { getStateCookie, createStateCookie } from "../stateCookie"

describe("createStateCookie", () => {
test("throws errors on empty string", () => {
expect(() => createStateCookie(""))
.toThrow("state value is required")
})

test("sets cookie with prefix app.txs", () => {
const state = createStateCookie("code_verifier_secret")
expect(document.cookie)
.toMatch(`app.txs.${state}=code_verifier_secret`)
})

test("sets cookie with latest value", () => {
const state = createStateCookie("code_verifier_secret")
expect(document.cookie)
.toMatch(`app.txs.${state}=code_verifier_secret`)
})

test("returns state", () => {
const state = createStateCookie("code_verifier_secret")
expect(state).toMatch(/[\w-]+/)
})
})

describe("getStateCookie", () => {
test("gets cookie", () => {
const state = crypto.randomUUID()
const random = Math.random().toString(36).substring(7)
document.cookie = `app.txs.${state}=${random}`
expect(getStateCookie(state)).toBe(random)
})

test("gets none", () => {
expect(getStateCookie('nothere')).toBeUndefined()
})
})
18 changes: 18 additions & 0 deletions src/lib/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const saveCookie = (name: string, value: string, mins: number = 60) => {
if (!name)
throw new Error('Cookie name is required')

const date = new Date()
date.setTime(date.getTime() + mins * 60 * 1000)
document.cookie = `${name}=${value};Expires=${date.toUTCString()}; path=/; Secure; SameSite=None`
}

export const getCookie = (name: string) => {
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) return parts.pop()?.split(';').shift()
}

export const deleteCookie = (name: string) => {
document.cookie = `${name}=; Max-Age=0; path=/; Secure; SameSite=None`
}
18 changes: 8 additions & 10 deletions src/lib/pkce.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { Sha256 } from '@aws-crypto/sha256-js'
export const toSha256 = async (text: string): Promise<number[]> => {
if (!text) throw new Error('data is required')

export const toSha256 = async (data: string): Promise<Uint8Array> => {
if (!data) throw new Error('data is required')
const encoder = new TextEncoder()
const data = encoder.encode(text)
const hashBuffer = await crypto.subtle.digest("SHA-256", data)

const hash = new Sha256()
hash.update(data)

const hashed = await hash.digest()
return hashed
return Array.from(new Uint8Array(hashBuffer))
}

// refer to base64url-encoding in RFC 7636
// https://datatracker.ietf.org/doc/html/rfc7636#appendix-A
export const toBase64Url = (bytes: Uint8Array) => {
export const toBase64Url = (bytes: number[]) => {
if (bytes.length === 0) throw new Error('bytes must not be empty')

const charCodes = Array.from(bytes)
Expand All @@ -38,7 +36,7 @@ export const createRandomString = (length: number = 34): string => {
}

export const createPKCECodeChallenge = async (codeVerifier: string): string => {
const hashed: Uint8Array = await toSha256(codeVerifier)
const hashed = await toSha256(codeVerifier)
const codeChallenge = toBase64Url(hashed)
return codeChallenge
}
15 changes: 15 additions & 0 deletions src/lib/stateCookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

import { saveCookie, getCookie } from './cookie'

export const createStateCookie = (value: string) => {
if (!value) throw new Error('state value is required')

const state = crypto.randomUUID()
saveCookie(`app.txs.${state}`, value, 30)

return state
}

export const getStateCookie = (state: string) => {
return getCookie(`app.txs.${state}`)
}