diff --git a/package-lock.json b/package-lock.json index cf29925..965a562 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "rtr-demo", "version": "0.0.0", "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -31,41 +30,6 @@ "webdriverio": "^8.38.2" } }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.577.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.577.0.tgz", - "integrity": "sha512-FT2JZES3wBKN/alfmhlo+3ZOq/XJ0C7QOZcDNrpKjB0kqYoKjhVKZ/Hx6ArR0czkKfHzBBEs6y40ebIHx2nSmA==", - "dependencies": { - "@smithy/types": "^3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -1162,52 +1126,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.0.0.tgz", - "integrity": "sha512-VvWuQk2RKFuOr98gFhjca7fkBS+xLLURT8bUjk5XQoV0ZLm7WPwWPPY3/AwzTLuUBDeoKDCthfe1AsTUWaSEhw==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@swc/core": { "version": "1.5.25", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.25.tgz", @@ -6010,7 +5928,8 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index 95132b5..9f2bf4e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "test": "vitest" }, "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/src/lib/__tests__/cookie.test.js b/src/lib/__tests__/cookie.test.js new file mode 100644 index 0000000..0f6bb74 --- /dev/null +++ b/src/lib/__tests__/cookie.test.js @@ -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") + }) +}) diff --git a/src/lib/__tests__/stateCookie.test.ts b/src/lib/__tests__/stateCookie.test.ts new file mode 100644 index 0000000..a15bc6d --- /dev/null +++ b/src/lib/__tests__/stateCookie.test.ts @@ -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() + }) +}) diff --git a/src/lib/cookie.ts b/src/lib/cookie.ts new file mode 100644 index 0000000..47aed71 --- /dev/null +++ b/src/lib/cookie.ts @@ -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` +} diff --git a/src/lib/pkce.ts b/src/lib/pkce.ts index bbe653d..e5759b3 100644 --- a/src/lib/pkce.ts +++ b/src/lib/pkce.ts @@ -1,18 +1,16 @@ -import { Sha256 } from '@aws-crypto/sha256-js' +export const toSha256 = async (text: string): Promise => { + if (!text) throw new Error('data is required') -export const toSha256 = async (data: string): Promise => { - 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) @@ -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 } diff --git a/src/lib/stateCookie.ts b/src/lib/stateCookie.ts new file mode 100644 index 0000000..e6cd8c0 --- /dev/null +++ b/src/lib/stateCookie.ts @@ -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}`) +}